阅读 210

V8引擎的垃圾回收过程,一遍过

内存空间分类

引用类型的数据存放在堆内存中,对堆内存的分配和垃圾回收由V8引擎执行。V8内垃圾回收策略主要基于分代式垃圾回收机制。

内存空间主要分为两类:新生代空间老生代空间

  • 新生代中存放一些在内存中驻留时间较短的对象,一般是临时分配的内存,来的快,去的也快。
  • 老生代空间中存放一些长期驻留内存的对象

V8内存空间示意图

在自动垃圾回收的演变过程中,人们发现,无法找到一种垃圾回收算法能够胜任任何场景。由于对象在内存中驻留时间长短不一,不同的算法只针对特定的场景具有最好的效果。因此,按照对象的存活时间,将垃圾回收进行不同的分代,再针对不同的分代施以最高效的算法。

垃圾回收算法

新生代——Scavenge算法

新生代空间被分为两个部分——FromTo

新生代内存空间示意图

From空间用来分配内存,垃圾回收算法执行时,会检查From空间中存活的对象,并将这些对象复制到To空间中,复制之前会做判断,如果满足一定条件会直接晋升至老生代空间。全部执行完毕之后,To空间中依次放置着存活的对象,此时互换这两个空间,又称翻转,将To空间作为新的From空间,将原本的From空间作为新的To空间,如此往复。

特点:由于其划分机制,实际只能使用新生代一半的空间,但由于新生代中存活对象较少,故其在效率上表现优异,是典型的牺牲空间,换取时间的算法。

晋升条件

如果一个对象经过多次垃圾回收后,依然存活,那么将会从新生代晋升到老生代中,使用另外的算法进行管理。具体晋升规则如下:

  1. 判断该对象是否经历过Scavenge算法的回收。
  2. 判断新生代的To空间占用是否超过25%。

这个25%的限制是因为,Scavenge算法回收结束后,To空间将变为From空间,若已使用内存占比过高,会影响之后的内存分配

老生代

Mark Sweep / Mark Compact

Mark Sweep(标记清除)、Mark Compact(标记整理)。

  1. 遍历老生代所有对象,将存活的对象打上标记
  2. 回收未标记的对象
  3. 当老生代的碎片空间不足以为从新生代晋升来的对象分配内存时,执行标记整理,清除内存碎片。

Mark Sweep最大的问题是回收后的内存会产生很多碎片,不利于接下来的内存分配,因此又提出了Mark Compact来整理内存。何为内存碎片?

回收之前
回收后

如图,白色区域为未分配内存。由于内存分配必须使用连续的地址,零散分布的内存,无法应对较大的对象,此时便需要使用标记整理:将存活的对象向一端移动,移动完成后,直接清理掉剩余的内存,完成整理和回收。

增量标记法——Incremental Marking

以上三种算法在执行时均需要暂停应用逻辑,等待垃圾回收执行完毕后恢复,这种行为被称为全停顿。新生代中,对象本身较少,即便全停顿影响也不大。但是老生代中存活对象通常很多,全堆垃圾回收的标记、清除、整理造成的停顿就比较可怕了。所以便有了增量标记法。

简单来说,增量标记法就是让垃圾回收和应用逻辑交替执行,将耗时较长的垃圾回收过程穿插在应用逻辑之间执行,就可以减少停顿感。这种方式可以将停顿时间减少到原本的1/6左右

V8后续还引入了延迟清理,增量整理等方式,让清理和整理也变为增量式。同时还计划引入并行标记与并行清理,进一步利用多核性能,降低每次停顿的时间。

对比

可以看出,Scavenge算法只赋值存活的对象,因为新生代中存活对象是少数;标记清除只回收死亡对象,因为老生代中,死亡对象是少数。因此保证了高效性。

算法 Mark Sweep Mark Compact Scavenge
速度 中等 最慢 最快
空间开销 少(有碎片) 少(无碎片) 双倍空间(无碎片)
是否移动对象

Mark SweepMark Compact中,V8主要使用前者,在内存不足一应付新晋升的对象时再使用Mark Compact

内存泄漏

垃圾回收是影响性能的因素之一,稍有不慎就可能造成内存泄漏。明白垃圾回收的原理有助于我们养成更好的编码习惯。此外,想要提高垃圾回收的执行效率,需要让垃圾回收尽可能少的执行,尤其是全堆的垃圾回收。

以Web服务器中的会话实现为例,一般通过内存存储,但访问量大时会导致老生代骤增,不仅清理/整理过程耗时,还会造成内存紧张甚至溢出,造成程序崩溃退出。

通常造成内存泄露的原因主要有:

  1. 缓存
  2. 作用域未释放(全局变量引用和闭包)

缓存

JavaScript的开发者总是喜欢用对象的键值对来作为缓存,但这与严格意义的缓存是有区别的。区别在于,严格意义的缓存有着完善的过期策略,而这种键值对的方式,通常没有。

如果需要使用这种方式做缓存,那么一定要设置一个缓存大小。如使用数组做缓存,限制其长度,并通过FIFO先进先出算法)来进行淘汰。但这种淘汰策略较为简单,效率也一般,只适合小型应用场景,更高效的缓存可以使用LRU最近最少使用算法)进行淘汰,算法具体内容就不展开了。

作用域释放

全局作用域直到进程退出才会释放,所以处在全局作用域环境下的变量总会被引用,因此无法被释放,除非主动释放:如delete删除属性,或显式赋值为undefinednull

闭包同理,使用需要小心,用的好是神器,用不好就是垃圾。

其他

关注下面的标签,发现更多相似文章
评论