V8内存管理及垃圾回收机制
JavaScript引擎的内存空间主要分为栈和堆。
栈
栈是临时存储空间,主要存储局部变量和函数调用。
基本类型数据(Number, Boolean, String, Null, Undefined, Symbol, BigInt)保存在在栈内存中。
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。
基本类型赋值,系统会为新的变量在栈内存中分配一个新值,这个很好理解。引用类型赋值,系统会为新的变量在栈内存中分配一个值,这个值仅仅是指向同一个对象的引用,和原对象指向的都是堆内存中的同一个对象。
对于函数,解释器创建了”调用栈“来记录函数的调用过程。每调用一个函数,解释器就可以把该函数添加进调用栈,解释器会为被添加进来的函数创建一个栈帧(用来保存函数的局部变量以及执行语句)并立即执行。如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。函数执行完成,对应的栈帧立即被销毁。
两种查看调用栈的方法
- 使用 console.trace() 向Web控制台输出一个堆栈跟踪.
- 浏览器开发者工具进行断点调试
栈虽然很轻量,在使用时创建,使用结束后销毁,但是不是可以无限增长的,被分配的调用栈空间被占满时,就会引起”栈溢出“的错误。
(function foo() {
foo()
})()
![](https://img.haomeiwen.com/i3297464/de7fa31f25fe7171.png)
为什么基本数据类型存储在栈中,引用数据类型存储在堆中?
JavaScript引擎需要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都存放在栈空间里面,会影响到上下文切换的效率,进而影响整个程序的执行效率。
堆
堆空间存储的数据比较复杂,大致可以划分为下面 5 个区域:代码区(Code Space)、Map 区(Map Space)、大对象区(Large Object Space)、新生代(New Space)、老生代(Old Space)。本篇文章主要讨论新生代和老生代的内存回收算法。
新生代内存是临时分配的内存,存活时间段,老生代内存是常驻内存,存活时间长。
![](https://img.haomeiwen.com/i3297464/be59143de584ecd1.png)
新生代内存回收
新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域(from),一半是空闲区域 (to)。
新的对象会首先被分配到 from 空间,当进行垃圾回收的时候,会先将 from 空间中的 存活的对象复制到 to 空间进行保存,对未存活的对象的空间进行回收。
复制完成后, from 空间和 to 空间进行调换,to 空间会变成新的 from 空间,原来的 from 空间则变成 to 空间。这种算法称之为 ”Scavenge“。
![](https://img.haomeiwen.com/i3297464/02be701e8fd103af.png)
新生代内存回收频率很高,速度也很快,但是空间利用率很低,因为有一半的内存空间处于"闲置"状态。
老生代内存回收
新生代中多次进行回收仍然存活的对象会被转移到空间较大的老生代内存中,这种现象称为晋升。以下两种情况
- 在垃圾回收过程中,发现某个对象之前被清理过,那么将会晋升到老生代的内存空间中
- 在 from 空间和 to 空间进行反转的过程中,如果 to 空间中的使用量已经超过了 25% ,那么就讲 from 中的对象直接晋升到老生代内存空间中。
因为老生代空间较大,如果仍然用 Scavenge 算法来频繁复制对象,那么性能开销就太大了。
标记-清除(Mark-Sweep)
老生代采用的是”标记清除“来回收未存活的对象。
分为标记和清除两个阶段。标记阶段会遍历堆中所有的对象,并对存活的对象进行标记,清除阶段则是对未标记的对象进行清除。
![](https://img.haomeiwen.com/i3297464/da6d13fc5af778f8.png)
标记-整理(Mark-Compact)
标记清除不会对内存一分为二,所以不会浪费空间。但是经过标记清除之后的内存空间会生产很多不连续的碎片空间,这种不连续的碎片空间中,在遇到较大的对象时可能会由于空间不足而导致无法存储。
为了解决内存碎片的问题,需要使用另外一种算法 - 标记-整理(Mark-Compact)。标记整理对待未存活对象不是立即回收,而是将存活对象移动到一边,然后直接清掉端边界以外的内存。
![](https://img.haomeiwen.com/i3297464/5d6a1d9f4471e752.png)
增量标记
为了避免出现JavaScript应用程序与垃圾回收器看到的不一致的情况,进行垃圾回收的时候,都需要将正在运行的程序停下来,等待垃圾回收执行完成之后再回复程序的执行,这种现象称为“全停顿”。如果需要回收的数据过多,那么全停顿的时候就会比较长,会影响其他程序的正常执行。
![](https://img.haomeiwen.com/i3297464/fe764e7be944cad0.png)
为了避免垃圾回收时间过长影响其他程序的执行,V8将标记过程分成一个个小的子标记过程,同时让垃圾回收和JavaScript应用逻辑代码交替执行,直到标记阶段完成。我们称这个过程为增量标记算法。
![](https://img.haomeiwen.com/i3297464/5d76cc018777dde3.png)
通俗理解,就是把垃圾回收这个大的任务分成一个个小任务,穿插在 JavaScript任务中间执行,这个过程其实跟 React Fiber 的设计思路类似。
参考
- V8 引擎垃圾内存回收原理解析
- JavaScript中V8引擎内存问题
- 浅谈V8引擎中的垃圾回收机制
- 《深入浅出Node.js》
其他
最近发起了一个100天前端进阶计划,主要是深挖每个知识点背后的原理,欢迎关注 微信公众号「牧码的星星」,我们一起学习,打卡100天。同时也会分享一些自己学习的一些心得和想法,欢迎大家一起交流。