【JS垃圾回收】带你探索垃圾回收机制和Chrome V8垃圾回收机制

news/2024/7/19 12:53:50 标签: javascript, js, 垃圾回收, 内存泄漏, chrome

文章目录

  • 前言
  • 一、什么是垃圾回收
  • 二、怎样进行垃圾回收
    • 1.标记清除
      • 优点
      • 缺点
    • 2.引用计数
      • 优点
      • 缺点
  • 三、Chrome V8垃圾回收机制
    • 1.为什么需要优化垃圾回收算法
    • 2.基本概念
    • 3.新生代垃圾回收器 - Scavenge
    • 4.老生代垃圾回收 - Mark-Sweep & Mark-Compact
      • Mark-Sweep
      • Mark-Compact
    • 5.并行回收
      • 全停顿
      • 并行
    • 6.增量标记
      • 增量
    • 7.懒性清理
  • 总结


前言

javaScript 是门魅力无限的语言,关于它的 GC(垃圾回收)方面,你了解多少呢?想来大部分人是因为面试才去看一些面试题从而了解的垃圾回收,我们做一件事,不应该只是为了知道而已,我们更应该取深究它的原理。接下来就让我们一起探索一下垃圾回收机制的奥秘吧。

一、什么是垃圾回收

1.基本思路

程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而垃圾回收就是负责回收垃圾的。

基本思路很简单,确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定的时间就会自动运行。

2.为什么要进行垃圾回收

程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃

二、怎样进行垃圾回收

1.标记清除

JavaScript中最常用的垃圾回收策略,就像它的名字一样,此算法分为标记清除两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁

整个标记清除算法大致过程就像下面这样

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0

  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间

  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

优点

实现比较简单,只存在打标记和不打标记两种情况,这种情况使得可以采用二进制01来进行标记

缺点

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
  • 分配速度慢,,其操作是一个 O(n)
    的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢

2.引用计数

这其实是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

整个引用计数算法过程就像下面这样:

  • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
  • 如果同一个值又被赋给另一个变量,那么引用数加 1
  • 如果该变量的值被其他的值覆盖了,则引用次数减 1
  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为0的值占用的内存

优点

引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾

缺点

可能会造成循环引用,无法进行垃圾回收导致内存泄露。ie8及之前版本的ie为什么闭包会导致内存泄露就是这个原因。大家有兴趣的话可以去看看我另一篇讲闭包会不会造成内存泄露的文章。闭包真的还会造成内存泄漏吗?

三、Chrome V8垃圾回收机制

1.为什么需要优化垃圾回收算法

在Chrome中,v8被限制了内存的使用(64位约1.4G/1464MB , 32位约0.7G/732MB),为什么要限制呢?

  • 表层原因是,V8最初为浏览器而设计,不太可能遇到用大量内存的场景
  • 深层原因是,V8的垃圾回收机制的限制(如果清理大量的内存垃圾是很耗时间,这样回引起JavaScript线程暂停执行的时间,那么性能和应用直线下降)

说到栈内的内存,操作系统会自动进行内存分配和内存释放,而堆中的内存,由JS引擎(如Chrome的V8)手动进行释放,当我们的代码没有按照正确的写法时,会使得JS引擎的垃圾回收机制无法正确的对内存进行释放(内存泄露),从而使得浏览器占用的内存不断增加,进而导致JavaScript和应用、操作系统性能下降。

2.基本概念

在JavaScript中,其实绝大多数的对象存活周期都很短,大部分在经过一次的垃圾回收之后,内存就会被释放掉,而少部分的对象存活周期将会很长,一直是活跃的对象,不需要被回收。为了提高回收效率,V8 将堆分为两类新生代和老生代,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收

3.新生代垃圾回收器 - Scavenge

在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。在新生代中,主要使用Scavenge算法进行垃圾回收

Scavenge算法将新生堆一分为二,一个是处于使用状态的空间我们暂且称之为使用区,一个是处于闲置状态的空间我们称之为空闲区。如下图:
在这里插入图片描述
工作方式可简述为:

  1. 新加入的对象都会放入使用区
  2. 对使用区的活动对象作标记
  3. 标记完成之后将使用区的活动对象复制进空闲区并进行排序
  4. 随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。也就是将使用区的非活动对象内存进行清除。
  5. 最后将使用区与空闲区进行对换。

可参照下图:
在这里插入图片描述

当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理

4.老生代垃圾回收 - Mark-Sweep & Mark-Compact

新生代空间中的对象满足一定条件后,晋升到老生代空间中,在老生代空间中的对象都已经至少经历过一次或者多次的回收所以它们的存活概率会更大,如果这个时候再使用scavenge算法的话,会出现效率低下和资源浪费的问题。所以使用Mark-Sweep 和 Mark-Compact算法来代替。

Mark-Sweep

Mark-Sweep处理时分为两阶段,标记阶段和清理阶段,看起来与Scavenge类似,不同的是,Scavenge算法是复制活动对象,而由于在老生代中活动对象占大多数,所以Mark-Sweep在标记了活动对象和非活动对象之后,直接把非活动对象清除。

  • 标记阶段:对老生代进行第一次扫描,标记活动对象
  • 清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象

这样其实会导致一个问题,就是被清除的对象会产生很多内存碎布分布在各个内存地址。

所以我们才有另一种算法 Mark-Compact 来解决这些内存碎片。

Mark-Compact

由上文可知,Mark-Sweep完成后,会存在大量的内存碎片,若不清理掉这些碎片的话,如果需要分配一个大对象,这里的碎片都无法完成分配,会提前触发垃圾回收机制。

为了解决内存碎片问题,Mark-Compact被提出,它是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存。

5.并行回收

全停顿

JavaScript 是一门单线程的语言,它是运行在主线程上的,那在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做全停顿。

那么就是每次垃圾回收时间多少秒,我们的逻辑就会停顿多少秒,如果垃圾回收时间过长,就会造成页面卡顿的现象。

并行

并行式垃圾回收允许主线程和辅助线程同时执行同样的工作,这样可以让辅助线程来分担主线程的工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量。

新生代对象空间就采用并行策略,在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域,这个过程中由于数据地址会发生改变,所以还需要同步更新引用这些对象的指针,此即并行回收。

6.增量标记

为了减少全停顿的时间,在 2011 年,V8 对老生代的标记进行了优化,从全停顿标记切换到增量标记

增量

增量就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记。

但我们想一想,这样会出现什么问题呢?

假如我们在一次完整的 GC 标记分块暂停后,执行任务程序时内存中标记好的对象引用关系被修改了又怎么办呢

在查阅资料后,V8 对这两个问题对应的解决方案分别是三色标记法与写屏障。由于自己不是很理解这两种解决方案,所以在此不作过多的解释,若感兴趣的话,大家可以查阅相应资料。

7.懒性清理

增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理。

懒性清理其实和常用的懒加载一样。都是为了让过程延迟执行。

增量标记完成后,惰性清理就开始了。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记。

最后,懒性清理也是有缺陷的。由于每个小的增量标价之间执行了JavaScript代码,堆中的对象指针可能发生了变化,需要使用写屏障技术来记录这些引用关系的变化

缺点:

  • 首先是并没有减少主线程的总暂停的时间,甚至会略微增加。

  • 由于写屏障机制的成本,增量标记可能会降低应用程序的吞吐量

总结

其实在现在,不同的浏览器都对垃圾回收算法进行了优化。我们通过V8的垃圾回收,一步一步深入来助我们了解垃圾回收机制,当我们真正了解垃圾回收机制时,才能更好的对内存泄漏类问题进行预防和优化!


http://www.niftyadmin.cn/n/861661.html

相关文章

深入透析Promise几种方法(含手撕思路讲解及坑点)

文章目录前言一、Promise.all()1.介绍2.实例状态全为fulfilled状态有一个为rejected3.代码实现思路代码二、Promise.race()1.介绍2.实例3.代码实现思路代码总结前言 前面我们简单实现了一个promise,不懂的同学,传送门:深入透析Promise 那么…

【Diff算法图解】带你探索React、Vue2.x的Diff算法

文章目录前言一、Virtual DOM(虚拟dom)二、React Diff实现思想移动节点增加节点移除节点React Diff的缺陷三、Vue2.X Diff实现思想移动节点特殊情况增加节点移除节点总结前言 我们都知道,在框架中,当dom节点发生变化时&#xff0…

CSS初学的简单样式

CSS导航条 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><style type"text/css">*{margin: 0;padding: 0;}.nav{list-style: none;background-color: blueviolet;width: 500px;margin:50p…

CSS风车

CSS 风车 添加边框后不改变原本设计的大小&#xff1a; Box-sizing:border-box&#xff1b;<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><style type"text/css">*{margin: 0;padding: 0…

ES6中的方法-1

ES6 的方法 1、let关键字* 在块级作用于有效* 不能重复声明* 不会预处理&#xff0c;不存在变量提升2、const关键字* 不能修改* 其他等同let3、变量解构赋值例&#xff1a;*对象解构let obj{username:kobe,age:39}let {username,age}objconsole.log(username)*数组解构let arr[…

实现Promise

在编写之前首先要了解一下promise有哪些特性: 构造函数有一个函数executor里面包含两个参数&#xff08;resolve,reject&#xff09;resolve成功时执行成功的回调reject失败时执行失败的回调三种状态 pending&#xff08;初始状态&#xff09;fulfilled&#xff08;操作成功&a…

antd上传图片及表单内容

antdnode.js实现图片上传&#xff08;图片和表单同提交&#xff09; 1、 前端实现 import React from react; import { connect } from react-redux; import { RightCircleOutlined, UploadOutlined } from ant-design/icons; import { Input, Form, Button, Upload } from a…

react系列 -react-router

一、spa(单页面应用)底层原理 1、喵点&#xff08;hash&#xff09; window.onhashchange 2、h5&#xff08;history&#xff09; 二、react -router 导入react-router-dom里面的三个组件 BrowserRoute/HashRouter 路由容器Route 需要该组件定义好路径和显式组建的对应关系…