垃圾回收机制是指将有限的内存中没用了的数据清理掉,腾出空间给新的数据。里头的难点有两:辨识没用了的数据,和清理方法及频率。
在看李兵大大讲解v8引擎是如何工作的时候,发现v8垃圾回收系统的设计优化比想象当中更复杂也更有意思些。以下是我额外翻阅一些资料后综合整理的笔记:
需要回收的垃圾
首先区分v8的两类内存空间:
- 栈空间(stack)- 即调用栈。存储执行上下文、原始数据类型(如string、number等)、及对象引用。
- 堆空间(heap)- 存储对象的类型、值。
其他还有代码空间之类的划分,感觉我查到的资料细节有些不一致就不多说了。
关于堆啰嗦两句帮助理解:
- 一种树结构,用于粗略排优先级。
- 最高或最低优先级的值永远保持在树根位置。添加、删除节点都需要调整树结构。
- 子节点的最大数量是固定的。
- 实际在存储中是用连续的分页的空间。也就是为什么通常堆会有字符串表达,如下图:
伙同上一点,在这个字符串中可以根据层级和子节点数计算出每个节点的位置。
那调用栈是用完就可以轻松扔掉回到上一层的,所以我们谈垃圾回收的时候,谈的是堆空间的清理。
v8垃圾回收的整体流程
v8在设计的时候发现所有内存用同一种方式处理的效益不高,于是在一种叫“代际假说(The Generational Hypothesis)”的指导下进行了优化。假说给内存里的数据对象分了个类:
- 长存不死的对象 —— 只占少数
- 和只短时间存在的对象 —— 大多数
据此,堆空间中又分了两块空间:
- 新生区(New space)——空间较小(约1~8MB可设置修改),清理频率高,使用副垃圾回收器。
- 老生区(Old space)——空间较大,清理频率低,使用主垃圾回收器。
这两个回收器使用的算法不同,但整体流程都可以概括为:
- 检查标记空间中存活和可以回收的对象;
- 回收对象占据的内存;
- 进行内存整理,减少内存碎片(记得计算机内存是分页的连续空间,对象存储通常也是占用连续的空间,碎片太多对大对象就用不了了)。
新生区用副垃圾回收器(Minor GC)
副垃圾回收器使用的方法叫Scavenger,算法叫Cheney’s Algorithm。
这个方法把新生区对半分为对象区(From space)、空闲区(To space)。新对象会尝试写入对象区,如果发现区域已满,就触发垃圾清理。
具体流程如下:
老生区用主垃圾回收器(Major GC)
主垃圾回收器用的则是标记-清除法(Mark-Sweap):
即:
- 标记——从根开始遍历。对还在需要被调用栈引用的对象和它们引用的对象们标记为活动,其余标为垃圾。这里具体的细节还没有挖清楚。
- 清除——把标为垃圾的对象占用的内存块标为空。
- 整理——剩余存活的对象向一端挪动,整理到一起。
那这些操作其实是很慢的。JS又只用一个主线程,做这样慢的垃圾回收操作会导致其他任何操作无法继续。因此v8又引入了其他一些优化方法,如:
- 增量回收——把整个操作切割成无数个小任务,与用户JS代码穿插运行。(这招类似React Fibre的任务调度)
- 并发标记过程——用多个辅助线程一起标记,避免用主线程。
- 并发清除及整理过程——同标记一样,用多个辅助线程,避免用主线程。
- 懒触发垃圾清理的过程——延迟清理。
小结
给程序倒个垃圾真不容易啊?!
其实还有许多细节在目前看到的资料里只是粗粗掠过,有待深挖。
参考资料
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END