站长资讯网
最全最丰富的资讯网站

聊聊V8的内存管理与垃圾回收算法

本篇文章带大家了解一下V8引擎的内存管理与垃圾回收算法,希望对大家有所帮助!

聊聊V8的内存管理与垃圾回收算法

众所周知,JS是自动管理垃圾回收的,开发者不需要关心内存的分配与回收。而且垃圾回收机制在前端面试中也是常考的部分。本文主要讲解V8的分代垃圾回收算法,希望阅读本文后的小伙伴能够对V8垃圾回收机制有个痛彻(哈哈,是痛彻!!!)的了解,文章主要涵盖如下内容:

  • V8的内存限制与解决办法
  • 新生代内存对象的Scavenge算法
  • 基于可达性分析算法标记存活对象的逻辑以及优化手段
  • 新生代内存对象的晋升条件、
  • Scavenge算法的深度/广度优先区别
  • 跨代内存的的写屏障
  • 老生代内存对象的标记清除/整理算法
  • GCSTW原因及优化策略

V8的内存限制与解决办法

V8最初为浏览器设计,遇到大内存使用的场景较少,在设计上默认对内存使用存在限制,只允许使用部分内存,64位系统可允许使用内存约1.4g,32位系统约0.7g。如下代码所示,在Node中查看所依赖的V8引擎的内存限制方法:

process.memoryUsage();  // 返回内存的使用量,单位字节 {   rss: 22953984,   // 申请的总的堆内存   heapTotal: 9682944,   // 已使用的堆内存   heapUsed: 5290344,   external: 9388 }

聊聊V8的内存管理与垃圾回收算法

V8限制内存使用大小还有另一个重要原因,堆内存过大时V8执行垃圾回收的时间较久(1.5g50ms),做非增量式的垃圾回收要更久(1.5g1s)。在后续讲解了V8的垃圾回收机制后相信大家更能感同身受。

虽然V8引擎对内存使用做了限制,但是同样暴露修改内存限制的方法,就是启动V8引擎时添加相关参数,下面代码演示在Node中修改依赖的V8引擎内存限制:

# 更改老生代的内存限制,单位mb node --max-old-space-size=2048 index.js  # 更改新生代的内存限制,单位mb node --max-semi-space-size=1024=64 index.js

这里需要注意的是更改的新生代的内存的语法已经更改为上述的写法,且单位也由kb变成了mb,旧的写法是node --max-new-space-size,可以通过下面命令查询当前Node环境修改新生代内存的语法:

node --v8-options | grep max

聊聊V8的内存管理与垃圾回收算法

V8垃圾回收策略

在引擎的垃圾自动回收机制的历史演变中,人们发现是没有一种通用的可以解决任何场景下垃圾回收的算法的。因此现代垃圾回收算法根据对象的存活时间将内存垃圾进行分代分代垃圾回收算法就是对不同类别的内存垃圾实行不同的回收算法。

V8将内存分为新生代老生代两种:

  • 新生代内存中的对象存活时间较短
  • 老生代内存中代对象存活时间较长或是常驻内存

新生代内存存放在新生代内存空间(semispace)中,老生代内存存放在老生代内存空间中(oldspace),如下图所示:

聊聊V8的内存管理与垃圾回收算法

  • 新生代内存采用Scavenge算法
  • 老生代内存采用Mark-SweepMark-Compact算法

下面我们看看Scavenge的算法逻辑吧!

Scavenge算法

对于新生代内存的内存回收采用Scavenge算法,Scavenge的具体实现采用的是Cheney算法。Cheney算法是将新生代内存空间一分为二,一个空间处于使用状态(FromSpace),一个空间处于空闲状态(称为ToSpace)。

聊聊V8的内存管理与垃圾回收算法

在内存开始分配时,首先在FromSpace中进行分配,垃圾回收机制执行时会检查FromSpace中的存活对象,存活对象会被会被复制到ToSpace,非存活对象所占用的空间将被释放,复制完成后FromSpaceToSpace的角色将翻转。当一个对象多次复制后依然处于存活状态,则认为其是长期存活对象,此时将发生晋升,然后该对象被移动到老生代空间oldSpace中,采用新的算法进行管理。

聊聊V8的内存管理与垃圾回收算法

Scavenge算法其实就是在两个空间内来回复制存活对象,是典型的空间换时间做法,所以非常适合新生代内存,因为仅复制存活的对象且新生代内存中存活对象是占少数的。但是有如下几个重要问题需要考虑:

  • 引用避免重复拷贝

假设存在三个对象temp1、temp2、temp3,其中temp2、temp3都引用了temp1,js代码示例如下:

var temp2 = {   ref: temp1, }  var temp3 = {   ref: temp1, }  var temp1 = {}

FromSpace中拷贝temp2ToSpace中时,发现引用了temp1,便把temp1也拷贝到ToSpace,是一个递归的过程。但是在拷贝temp3时发现也引用了temp1,此时再把temp1拷贝过去则重复了。

要避免重复拷贝,做法是拷贝时给对象添加一个标记visited表示该节点已被访问过,后续通过visited属性判断是否拷贝对象。

  • 拷贝后保持正确的引用关系

还是上述引用关系,由于temp1不需要重复拷贝,temp3被拷贝到ToSpace之后不知道temp1对象在ToSpace中的内存地址。

做法是temp1被拷贝过去后该对象节点上会生成新的field属性指向新的内存空间地址,同时更新到旧内存对象的forwarding属性上,因此temp3就可以通过旧temp1forwarding属性找到在ToSpace中的引用地址了。

内存对象同时存在于新生代和老生代之后,也带来了问题:

  • 内存对象跨代(跨空间)后如何标记
const temp1 = {}  const temp2 = {   ref: temp1, }

比如上述代码中的两个对象temp1temp2都存在于新生代,其中temp2引用了temp1。假设在经过GC之后temp2晋升到了老生代,那么在下次GC的标记阶段,如何判断temp1是否是存活对象呢?

在基于可达性分析算法中要知道temp1是否存活,就必须要知道是否有根对象引用引用了temp1对象。如此的话,年轻代的GC就要遍历所有的老生代对象判断是否有根引用对象引用了temp1对象,如此的话分代算法就没有意义了。

解决版本就是维护一个记录所有的跨代引用的记录集,它是写缓冲区的一个列表。只要有老生代中的内存对象指向了新生代内存对象时,就将老生代中该对象的内存引用记录到记录集中。由于这种情况一般发生在对象写的操作,顾称此为写屏障,还一种可能的情况就是发生在晋升时。记录集的维护只要关心对象的写操作和晋升操作即可。此是又带来了另一个问题:

  • 每次写操作时维护记录集的额外开销

优化的手段是在一些Crankshaft操作中是不需要写屏障的,还有就是栈上内存对象的写操作是不需要写屏障的。还有一些,

赞(0)
分享到: 更多 (0)
网站地图   沪ICP备18035694号-2    沪公网安备31011702889846号