一晃眼到了2020年下半年,之前开通的blog现在已经长草了。懊恼之余,还是痛定思痛,奋起直追,好好写写博客和专栏。
今天聊聊JavaScript的垃圾回收机制
首先明确一点,不同于C、C++这类需要人工管理内存的语言,JavaScript里的内存管理是自动进行的。正因如此,才有了今天我们介绍的GC(Garbage Collection,垃圾回收)算法。所谓垃圾回收机制,意思是把内存中不再有用的空间进行释放并再次利用起来。为了实现这个逻辑,在引擎内部势必存在一种判断内存空间是否“不再有用”的标准,这种不同的标准即不同的GC算法。在JavaScript实现中出现过多种GC算法,下面我们列举出最常见的几种。
1. 引用计数算法
核心思想:在内部设置一个引用计数器,当每个对象空间的引用关系发生变化时,修改计数器的数值。当某个对象空间的引用数为0时,
会立即触发垃圾回收机制,对这个对象空间进行回收。
引用计数算法出现在早期的实现中,现在已经很少被用到。但我们依然可以通过学习这种算法,了解它的思想。下面举个例子来说明:
let obj = {name:'zhangsan'} // 对象zhangsan被obj引用了一次
let obj2 = obj1 // 对象zhangsan被第二次引用,计数器值为2
obj = null // 解除obj到对象zhangsan的引用,zhangsan的引用计数器值变为1
obj2 = null // 此时对象zhangsan的引用变为0,堆内存中存储zhangsan的空间将被当成垃圾回收
引用计数算法优缺点说明
优点:
- 由于引用计数算法实时监控着内存空间的引用数,当引用数为0时,能立即回收该内存空间
- 引用计数算法能最大程度减少程序的暂停
缺点:
- 由于引用计数算法维护着一个计数器,它要时刻监控该数值是否需要修改,因此时间开销较大。
- 不能解决循环引用的问题
所谓的循环引用,可以用一个例子说明:
let obj1={value:obj2}
let obj2={value:obj1}
// 这样就构成了一对循环引用。即使obj1和obj2所代表的对象空间在其他地方再没有被引用到,
// 由于他们互相引用,这两个内存空间也不会被销毁。
2. 标记清除算法
核心思想:将垃圾回收过程分为 标记 和 清除 两个阶段。在标记阶段,从根对象(全局对象)出发遍历所有对象,将所有可达对象
做上标记。在清除阶段,同样会遍历所有对象,对没有标记的对象进行清除操作。
这段定义里有两个关键概念:根对象 和 可达对象,下面依次给出解释:
- 根对象:在JavaScript中可以认为是全局对象
- 可达对象:从根对象出发,能够通过层层引用被访问到的对象。 好比全局对象上定义了一个变量obj1,其内部属性value指向了另一个对象obj2,在obj2中,又引用了obj3。在这条引用链上的所有对象,obj1,obj2,obj3都是可达对象,反之,如果某个对象不能通过根对象的引用找到,则为不可达对象,其内存空间将被当做垃圾回收。
标记清除算法优缺点说明
优点:
可以回收循环引用的对象
缺点:
- 容易产生碎片化空间,浪费空间
- 不会立即回收垃圾对象
3.标记整理算法
标记整理算法是一种运用于v8引擎中的GC算法,它可以看做是标记清除算法的升级版。
标记整理算法分为 标记 和 整理 两个阶段。其中的标记阶段和 标记清除 算法相同,都是将所有可达对象做上标记。 在整理阶段,将所有标记过的可达对象在内存空间上进行移动,使其占有连续的内存空间。
应用: V8中的垃圾回收策略
V8引擎是chrome浏览器内部和Node的JS执行引擎,其特性是运行高效,即时编译,并且内存设置了上限
- 对64位操作系统上: 上限大约为1.5G
- 对32位操作系统上: 上限大约为800M
V8引擎的垃圾回收采用了 分代回收 的回收策略
在V8内部,把内存空间分成了新生代和老生代区域,针对不同代采用不同的GC算法
其中新生代区域空间较小,在64位操作系统上为32M,在32位操作系统上为16M,其中保存着存活时间较短的对象。
v8将新生代空间均分为两个等大空间,使用中的空间称为from,空闲的空间称为to。当from空间使用达到一定上限,就会触发垃圾回收机制。V8新生代的垃圾回收采用的是Scavenge策略。
新生代对象回收实现
-
标记阶段。将from空间中的活跃对象(使用中对象)进行标记,识别出等待回收的对象
-
排序整理阶段。
- 将from空间中的活跃对象拷贝到to空间中
- 将from空间完全释放
- 将from空间中的活跃对象拷贝到to空间中
-
交换阶段。 将from空间和to空间进行互换,完成本次垃圾回收操作。
与新生代区域不同,老生代区域存放的是活动时间较长的对象,比如全局对象、闭包等。
与新生代区域一样,老生代内存区同样也存在大小限制,和进行垃圾回收的特有策略
在64位操作系统中,老生代内存的存储上限是1.4G
在32位操作系统中,老生代内存代存储上限是700M
新生代对象向老生代晋升
当新生代中一个对象多次出现在to空间里,或者当to空间内存超过25%时,该对象会被移入老生代空间。这种操作被成为晋升。
默认情况下,程序生成的对象会首先被放置在from空间中。当进行垃圾回收,将对象们从from移至to时,会通过检查该对象的内存地址来判断该对象是否已经经历过一次Scavenge运算,如果是,则将该对象移至老生代空间。接下来,判断to空间是否超过25%,如果是,则仍将该对象移至老生代空间。
老生代对象回收实现
新生代对象中之所以能使用Scavenge算法这种利用空间来交换时间的算法,是因为新生代对象存储空间本身比较小,即使被一分为二,损失的存储空间也很有限。相比新生代的这种策略,老生代对象的存储空间更大,使用分代回收的话将会损失很大的存储空间,得不偿失。另外,由于老生代内存存储了大量对象,用这种算法复制对象,会使效率大幅降低。
因此,老生代内存的垃圾回收,采用了与新生代不同的策略。具体来说,主要有标记清除(mark-sweep)和标记整理(mark-compact)两种算法。没错,就是文章前面提到的两种基本算法。
在此,我们用图示进一步说明这两种算法:
-
标记清除算法在老生代空间内的应用:
-
开始阶段,其中ABCDEF为已使用内存
-
标记阶段,其中ACE为活跃对象,其余为待清除对象
-
清除阶段,将待清除对象的空间释放
由以上图示不难发现,标记清除算法在每次清除过后,内存会出现不连续的状态。后续继续在老生代空间里分配内存时,如果需要分配一个大内存,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
因此,在标记清除算法的基础上,又发展出标记整理算法。标记整理算法在标记阶段和清除算法是一样的。不同的是,在标记完活跃对象后,会将活跃对象移动到堆内存的另一端,然后将边界外的内存全部清除。
-
-
标记整理算法在老生代空间内的应用:
- 标记阶段,和标记清除算法一致。
- 整理阶段,将活跃对象拷贝直堆内存的另一端
- 清除阶段,将拷贝对象边界外的内存清空
- 标记阶段,和标记清除算法一致。
-
标记清除算法和标记整理算法的结合使用
在取舍上,由于标记整理算法需要移动对象,所以速度不会很快;因此,老生代算法主要采用标记清除,当新生代晋升过来的对象大小大于老生代可用空间时,才启动标记整理算法。
总结
以上就是关于JS垃圾回收机制的内容。总的来说,JavaScript中存在多种GC算法,在V8引擎中,主要利用了标记清除、标记整理和scavenge算法,分别对新生代和老生代内存进行垃圾回收。
-
其中新生代采用scavenge算法,利用空间的损失换取时间上的优势。
-
老生代混合采用标记清除和标记整理算法,对保存至老生代空间的对象进行回收。
-
新生代空间内的对象满足一定条件,可用晋升至老生代空间。
-
另外在老版本的js引擎中,还使用过引用计数算法,目前已经不再使用。
写作时参考到的文章,感谢你们的帮助: