GC垃圾回收机制

js的垃圾收集

垃圾回收机制:找出不再继续使用的变量(生命周期结束),然后释放其内存。垃圾回收器会按固定的时间间隔周期性地执行

自动管理:找出不再继续使用的变量,然后释放其占用的内存。垃圾收集器会按照固定的时间间隔周期性地执行此操作。

函数中局部变量的正常生命周期:局部变量只在函数执行的过程中存在

在函数的执行过程中,会为局部变量在栈/堆中分配响应的空间,以便存储它们的值,然后在函数中使用这些变量,直到函数执行结束,释放内存以供将来使用。垃圾收集器必须跟踪哪个变量有用哪个变量没用,对不再有用的变量加上标记,以便将来回收其占用的内存。用于标记无用变量的策略/垃圾收集的方式:

1. 标记清除(常用)

  • 通过翻转某个特殊的位来记录一个变量何时进入环境
  • 使用一个进入环境的变量列表及一个离开环境的变量列表来跟踪哪个变量发生了变化

当变量进入环境时,就将这个变量标记为进入环境,永远不能释放进入环境的变量所占用的内存,当变量离开环境时,将其标记为离开环境

2. 引用计数

跟踪记录每个值被引用的次数

当声明了一个变量并将一个引用类型值赋值给该变量时,则这个值的引用次数就是1,如果同一个值又被赋给另外一个变量,则这个值的引用次数+1。反之,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数-1。当这个值的引用次数变为0时,就说明没有办法再访问这个值了,就可以将其占用的内存回收回来。当垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

循环引用的问题

对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的指针。

1
2
3
4
5
function problem(){
var objA= new Object()
var objB= new Object()

}

JS中的栈和堆

  1. 栈:基本数据类型

    占据空间小,大小固定、属于被频繁使用的数据

  2. 堆:引用类型(Object、Array、Function)

    占据空间大,大小不固定。如果存储在栈中,会使程序性能下降。引用数据类型在栈中存了指针。

内存泄漏

一块被分配的内存既不能使用,又不能释放,直到浏览器进程结束

  • 意外的全局变量(使用严格模式可避免)
  • 闭包
  • 没有清除对DOM的引用

    在文档树中remove了,但是JS某个对象嗨保留着对元素的引用或者DOM元素被清除而绑定的事件未清除。

1
2
3
4
5
6
function assignHandler(){
var element= document.getElementById('some')
element.onclick= function(){
console.log(id)
}
}
1
2
3
4
5
6
7
8
function assignHandler(){
var element= document.getElementById('some')
var id= element.id
element.onclick= function(){
console.log(id)
}
element.id= null
}

V8 下的垃圾回收机制

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。

新生代算法

新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代算法

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。

在讲算法前,先来说下什么情况下对象会出现在老生代空间中:

新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
老生代中的空间很复杂,有如下几个空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不变的对象空间
NEW_SPACE, // 新生代用于 GC 复制算法的空间
OLD_SPACE, // 老生代常驻对象空间
CODE_SPACE, // 老生代代码对象空间
MAP_SPACE, // 老生代 map 对象
LO_SPACE, // 老生代大空间对象
NEW_LO_SPACE, // 新生代大空间对象

FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下情况会先启动标记清除算法:

某一个空间没有分块的时候
空间中被对象超过一定限制
空间不能保证新生代中的对象移动到老生代中
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行,你可以点击 该博客 详细阅读。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存。