学习内存 缓存和垃圾回收相关知识
在 V8 中,所有 JavaScript 的对象都是通过 堆来分配 的。
当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆大小超过V8的限制。
那为什么 V8 要限制堆大小?
- 表层原因:V8 最初为浏览器而设计,无大内存场景。
- 深层原因:V8 垃圾回收机制限制。以 1.5GB 的垃圾回收为例,V8 做一次小的垃圾回收需要 50 毫秒以上,而一次非增量式垃圾回收需要 1 秒以上。垃圾回收引起 JS 线程暂停执行,这么长时间是不可接受的。
V8 的垃圾回收机制
V8 的垃圾回收策略主要基于 分代式垃圾回收 机制。因为实际应用中,对象的生存周期长短不一,不同的算法只针对特定情况有最好效果,所以现代垃圾回收算法中,按对象存活时间将内存的垃圾回收进行不同的分代,然后分别运用不同算法。
V8 的内存分代
V8 主要将内存分为 新生代 和 老生代 两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
新生代:Scavenge 算法
新生代的对象主要通过 Scavenge 算法进行垃圾回收。而 Scavenge 的具体实现中,采用 Cheney 算法—— 一种采用复制方式实现的垃圾回收算法:
- 将堆内存一分为二,每个部分空间称为 semispace;
- 两个 semispace 一个处于使用中(称为 From 空间),一个处于空闲状态(称为 To 空间);
- 当我们分配对象时,在 From 空间进行分配;
- 当开始进行垃圾回收时,会检查 From 空间的存活对象,把它们复制到 To 空间,而非存活对象占用的空间被释放;
- 完成复制后,From 空间和 To 空间角色对换。
Scavenge 的缺点是只能使用堆内存的一半,但由于只复制存活对象,并且由于生命周期短的场景中存活对象只占少部分,所以它在时间效率上不错。
当一个对象经过多次复制依然存活时,它会被认为是生命周期较长的对象,会被移动到老生代中,采用新的算法进行管理。对象从新生代移动到老生代称为晋升。
不同于单纯的 Scavenge 过程,在分代式垃圾回收的前提下,From 空间的存活对象复制到 To 空间前需要进行检查:即是否可以晋升。
晋升的两个条件:
- 对象是否经历过(一次) Scavenge 回收;
- To 空间的内存占用超过一定比例,比如 25%。设置比例是因为此次 Scavenge 回收完成后, To 空间将变成 From 空间,占用比例过高将影响后续内存的分配。
老生代:Mark-Sweep & Mark-Compact
对于老生代,由于存活对象占比高,采用 Scavenge 会有两个问题:
- 存活对象多,复制效率会很低;
- 依然存在浪费一半空间的问题。
所以老生代采用 Mark-Sweep 和 Mark-Compact 相结合的方式进行垃圾回收。
Mark-Sweep,即标记清除 ,分为标记和清除两个阶段。
- 标记阶段,遍历对中所有对象,并标记活着的对象。
- 清除阶段,只清除没有被标记的对象。
相比 Scavenge ,Mark-Sweep 不存在浪费空间的行为,只清理死亡对象。
当 Mark-Sweep 最大的问题是在 进行一次标记清除回收后,内存空间会存在不连续的状态 。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次垃圾回收是不必要的。
Mark-Compact 可以解决内存碎片的问题,Mark-Compact 是 标记整理 ,在 Mark-Sweep 基础上演变而来。它们的差别在于在标记后,在整理的过程中,将活着的对象向一端移动,移动完成后,直接清理掉边界外的内存。
Incremental Marking 增量标记
为避免出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的情况,以上 3 种垃圾回收算法都需要将应用逻辑暂停,执行回收后再运行——即全停顿(stop-the-world)。
为了降低全堆垃圾回收带来的停顿时间,V8 从标记阶段入手,将全量标记改为增量标记,垃圾回收与应用逻辑交替执行,直到标记阶段完成。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
Java Garbage Collection Basics
可配合Java垃圾收集对照阅读。可以看到,垃圾收集原理都是一致的。