V8 的垃圾回收机制(重点)
V8 实现了准确式 GC GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
新生代算法
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法,它是一个复制算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。
新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是 标记清除算法 和 标记压缩算法。
处于以下情况的对象会出现在老生代空间中:
- 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
- To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
老生代中的空间很复杂,有如下几个空间:
enum AllocationSpace {
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
};
在老生代中,以下情况会先启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
在这个阶段,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
通过代码优化减少 GC 压力
- 避免频繁创建临时对象
- 合理使用闭包
- 及时清理定时器和事件监听器
会造成内存泄露的操作有哪些?(重点)
- 由于使用未声明的变量,而意外创建了一个全局变量,这个变量一直留在内存中无法被回收。
- 设置了
setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 - 获取一个
DOM元素的引用,而后面这个元素被删除,由于一直保留了对这个DOM元素的引用,所以它也无法被回收。 - 不合理地使用闭包,从而导致某些变量一直被留在内存当中。
总结
本文详细介绍了 V8 引擎的垃圾回收机制,包括分代式垃圾回收的核心思想、新生代的 Scavenge 算法和老生代的标记清除与标记压缩算法。同时,也总结了常见的内存泄露场景及其原因。理解这些内容对于前端开发者优化代码性能、避免内存问题至关重要。