垃圾回收
垃圾回收分为手动回收和自动回收两种策略。js 需要垃圾回收吗?js 不像 java,c 对内存的管控可以自己来操作,不需要就可以通过 free 就释放即可。何时分配,何时销毁内存都是由代码来控制的。
如果创建了内存,而没有进行 free 回收。这种情况就被称为内存泄露。
js 中如何回收
js 的数据的存放是存储在栈和堆两种内存空间的。 自然,也就分为栈中的垃圾回收和堆中的垃圾回收。
栈中垃圾回收
function foo() {
var name = '小明';
function bar() {
}
bar();
}
当执行到 bar 函数,就会创建 bar 函数的执行上下文,将 bar 压入到调用栈中。还有一个记录当前执行状态的指针(ESP),指向 bar 函数的执行上下文,表示当前正在执行 bar 函数。
bar 函数执行完,就要销毁 bar 函数的上下文,ESP就会下移到 foo 函数上下文,这个下移操作就是销毁 bar 函数执行上下文的过程。
只是做了下移动操作,等到下一次重新创建 bar 的上下文,就会将旧的上下文就行直接覆盖,也就可以销毁。
堆中的数据回收
即使栈中的数据被销毁了,但是堆中的对象仍然占用着空间。
要回收堆中的垃圾数据,需要使用 js 中的垃圾回收器。
垃圾回收的算法有很多种,但是没有哪一种能胜任所有的场景,需要权衡各种场景,根据对象的生存周期的不同而使用不同的算法,以达到最好的效果。
在 V8 中把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的是时间久的对象。
新生区通常只支持 1~8M的容量,老生区的容量就大很多了。 V8分别使用两个不同的垃圾回收器,以便高效地实施垃圾回收。
新生代的垃圾回收使用由 副垃圾回收器负责。
主垃圾回收器老负责老生代的垃圾回收。
垃圾回收流程。
-
标记空间中活动对象和非活动对象。
-
回收非活动对象所占据的内存。
-
内存整理。频繁回收,会导致内存出现大量不连续的空间。将不连续的内存空间称为内存碎片。 但是副垃圾回收器不会产生内存碎片。
副垃圾回收器
大多数小的对象都会被分配到新生区,使用比较频繁。
新生代中使用 Scavenge 算法。这个算法,会把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
新加入的对象都会放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,首先对对象区域的垃圾做标记;标记完,进入清理操作,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序的排列起来。复制后空闲区域就没有内存碎片了。
完成复制后,对象区域与空闲区域进行角色翻转,完成了垃圾回收的操作,可以无限的将这块区域使用下去。
因为 Scavenge 算法,需要复制操作,为了执行效率,一般新生区都设置的娇小。
但是空间小,会导致很容易填充满。js 引擎采用了对象晋升策略。只要经过两次垃圾回收仍然存活的对象,就会被移动到老生区中。
主垃圾回收器
老生区的特点就是一是对象占用空间大,另一个是对象存活时间长。
主垃圾回收器采用 标记-清除的算法进行垃圾回收。
首先进行标记,标记是从一组根元素开始,递归的遍历这组根元素,在这个遍历过程中,能够到达的元素称为活动对象,没有到达的元素就是垃圾数据。
标记清楚算法后,会产生大量不连续的内存碎片。碎片过多导致大对象无法分配到足够的连续内存,产生了一种算法: 标记-整理。
它不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
全停顿
js 运行在主线程,一旦执行垃圾回收算法,都需要将正在执行的 js 脚本全部暂停下来,等待垃圾回收完毕后再恢复脚本执行。
这样主现场不能够做任何事情,就会造成卡顿。 新生代比较小,所以影响不大,但是老生代占用的空间大,全停顿的影响大。
为了降低老生代带来的影响, V8 将标记过程分为一个个子标记过程,同时让垃圾回收标记和 js 应用逻辑交替进行,直到标记阶段完成。这个算法就是 增量标记算法。
可以把一个完整的垃圾回收拆分为很多小的任务。可以穿插到其他 js 任务中间执行。这样带来的卡顿就比较小了。