JS垃圾回收机制
一、什么是垃圾回收
在说垃圾回收之前,我们首先需要了解的是,什么是垃圾?为什么要进行垃圾回收?
- 已经调用完毕的函数作用域及其内部的值
- 值为 null 值
- 无法被访问到的值
上面已经说了,JS中的所有的变量都会占用内存,当这些变量变成垃圾的时候,如果不进行回收,内存就会被一直占用,随着程序的运行,垃圾也会越来越多,总有一刻,内存会被占满,程序也就无法运行了
垃圾回收: 程序在运行过程中会产生很多垃圾,这些垃圾会占用很多的内存,JS引擎会定期对这些垃圾进行清除
二、垃圾回收机制
JavaScript
内置的引擎中有一个叫做垃圾回收器的程序,这个程序每隔一段时间就会执行一次
在 JavaScript
中有一个很重要的概念叫做可达性
,即以任意一种方式,只要能被访问到的值,就继续让它保存在内存中,否则,说明这个值已经不被引用,需要被清理
基于这个思路,有以下两种常见的回收策略
1. 引用计数
跟踪记录每个值被引用的次数
- 当一个变量被声明,并将一个引用类型值赋值给它时,这个引用类型值的引用次数就为
1
- 如果这个值又被赋值给了另一个对象,则这个值的引用次数就
+1
- 如果被赋值的变量又有了新的值,也就是说这个值被其它值覆盖了,则引用次数
-1
当这个值的引用次数变为 0
的时候,说明这个值已经无法被访问,这个值所占的内存也就需要被回收,当下一次垃圾回收器运行时,就会清理掉这个值
听起来这是一个很不错的策略,但是这个方法有一个很严重的缺陷,就是循环引用
时引用值永远不为 0
let objA = new Object();
let objB = new Object();objA.a = objB;
objB.b = objA;
在上面的案例中,objA
和 objB
相互调用,它们的引用计数值都为 2
,因此,这两个值也就永远无法被垃圾回收机制清除
2. 标记清除
这是目前大部分浏览器中的 JavaScript 引擎
都在采用的方法
垃圾回收机制运行时会将所有的变量都加上一个标记,假设为 ×
,表示待删除,然后遍历一遍所有的变量,将所有能够访问到值的变量标记去掉,剩下那些含有 ×
标记的变量全部清除,回收其所占用的内存
标记清除法当然也有其局限性
- 比如每次运行垃圾回收器都要遍历一次所有的变量,这是一件比较消耗性能的事,而且垃圾回收器运行的频率又该如何去定,频率太高影响性能,频率太低垃圾无法及时回收
- JS是单线程语言,垃圾回收器运行时,会阻塞其它程序的运行,可能会造成页面卡顿(当项目很大,内存占用较多,清理需要较长时间时,有可能会感知到卡顿)
- 清理完成后,原本垃圾所占用的内存是碎片化的,位置不会发生改变,虽然清理了内存,但新的对象如果想要存入这些内存中,就需要先遍历一遍这些碎片内存,确定哪些内存容量合适,再选择一个容量差不多的
三、V8对垃圾回收的优化
我们已经知道,大部分浏览器使用的都是标记清除
法,所以 V8 引擎
对垃圾回收的优化,针对的也是标记清除法
上面已经提到了,标记清除法有着一些局限性,V8针对这些局限性,进行一些优化,它的优化策略如下
将内存分为两部分,分别叫 新生代
和 老生代
,新生代又分为 正在使用的区域
和 闲置区域
,我们知道,垃圾回收器的运行是按照一定的频率反复执行的,这期间有一些变量可能经历了多次回收依然存在,这说明该变量可能在很长一段时间内都不会被回收,如果每次执行垃圾回收都要遍历它,尤其是当这个变量内容很多时,这是一件既耗时又没有意义的事
V8 引擎将那些经历多次垃圾回收依然存在的变量放入到 老生代
中保存,以及那些占用内存比较大的变量变量也放入老生代中,其余变量放入新生代中,老生代执行垃圾回收的频率较低,新生代较高,每次对新生代
执行垃圾回收时,对正在使用的区域
进行标记,标记完成后复制到闲置区域
中,在闲置区域中进行清除并对内存进行整理排序,完成后再把闲置区域转换成使用区域,原本的使用区域转成闲置区域,并将其中的所有值全部清除
- 经过多次垃圾回收依然存在的对象放到
老生代
内存中 - 单个对象占用超过新生代内存
25%
的放入老生代
内存中 - 新生代的内存容量一般在
1 - 8M
区间 - 在使用区域进行标记,在闲置区域进行回收和整理,完成后互换身份,并将新的闲置区域内存清空(因为清理是在闲置内存中进行,避免了阻塞)
- 除了定期运行垃圾回收器,当新生代内存即将占满时,也会立即触发垃圾回收器
其实上面这样描述是不太准确的,JS
中的原始值
会存在栈内存
中,引用值
存在堆内存
中,将引用值赋值给变量时,并不是把这个引用对象重新复制一份,而且创建一个指向它在堆内存中位置的指针
,将这个指针存入栈内存中,垃圾回收主要针对的是堆内存,虽然这样描述更准确,但却不容易理解,所以如果不能理解这段话,仅看上面那段即可