主题
垃圾回收
以下是 v8引擎 的实现
V8 的内存结构
新生代(new_space)
:大多数的对象开始被分配的地方,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来老生代(old_space)
:新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低大对象区(large_object_space)
:存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区代码区(code_space)
:代码对象,会被分配在这里,唯一拥有执行权限的内存区域map区(map_space)
:存放Cell和Map
新生代
采用 Scavenge
算法,它将新生代内存一分为二,每一个部分的空间称为semispace
,其中处于激活状态的区域称为From
空间,未激活(inactive new space)的区域称为To
空间。这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。声明的对象首先会被分配到From
空间,当进行垃圾回收时,如果From
空间中尚有存活对象,则会被复制到To
空间进行保存,非存活的对象会被自动回收。当复制完成后,From
空间和To
空间完成一次角色互换,To
空间会变为新的From
空间,原来的From
空间则变为To
空间
这种算法是用空间换时间,虽然划分了一半内存保存存活变量,但有助于时间效率
对象晋升
以下两个条件都会使对象晋升,即从新生代转移到老生代
- 经历过一次
Scavenge
算法 To
空间的内存占比已经超过25%
老生代
老生代管理着大量存活对象,所以不使用Scavenge
算法,而是使用Mark-Sweep(标记清除)
和Mark-Compact(标记整理)
Mark-Sweep(标记清除)
分为标记
和清除
两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。Mark-Sweep
算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:
垃圾回收器会在内部构建一个
根列表
,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window
全局对象可以看成一个根节点。然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。
由于JS的单线程机制,垃圾回收的过程会阻碍主线程同步任务的执行。V8引擎引入Incremental Marking(增量标记)
,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存,避免应用卡顿,提升应用性能。
引用计数
2012 年前浏览器使用的垃圾回收方式
思路
- 对每个值都记录它被引用的次数。
- 声明变量并给它赋一个引用值时,这个值的引用数为 1
- 如果一个值又被赋给另一个变量,那么引用数加 1
- 如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1
- 如果引用数为 0,则说明可以安全地收回其内存
可能造成回收的不了的问题:循环引用
js
function func() {
let objA = new Object()
let objB = new Object()
objA.b = objB
objB.a = objA
}
如上例子,objA、objB 应该在函数结束调用时,就没用了,应该被回收。但实际上他们的引用数都为 2,不会被回收。
现代引擎都是使用标记清理
提升性能
垃圾回收是一个周期性的过程,基本上都是根据已分配对象的大小和数量来判断是否进行垃圾回收的。将内存占用量保持在一个较小的,可以让页面性能更好。
1. 解除引用
- 对于全局变量和全局对象的属性。如果数据不再必要,那么把它设置为 null ,从而释放其引用
2. 尽量使用 const 和 let
- 因为这两个变量都以块为作用域,相比于 var ,可以更早的让垃圾回收介入
3. 少创建全局变量
- 全局变量会挂载到 window 上,一直能被访问的到,所以不会被回收
4. 减少闭包使用
- 需要内部函数的作用域被释放,闭包保存的变量才能被释放
5. 使用弱引用