JavaScript的垃圾回收机制简述


新生代 semispace

新生代 semispace

老生代


一、内存限制

这里主要是探讨V8引擎的垃圾回收机制。先说一下内存限制,在64位系统中Node大约能使用1.4GB内存空间,32位系统为0.7GB左右,这就导致Node无法直接操作大内存对象。

二、对象分配

在V8中,所有的JavaScript对象都通过来进行分配,使用Node的process.memoryUsage()函数可以查看内存使用量。需要注意的是,查询到的内存总量是动态分配的:当新建一个对象时发现内存不够用,则向系统申请新的空间,直到达到系统限制。实际上Node提供了接口供我们修改允许的最大内存使用量,但是这个值也不能动态修改。

三、垃圾回收机制

V8的垃圾回收机制主要基于分代式垃圾回收机制,对不同的分代采取不同的回收算法,以提高性能。

1. 新生代

新生代主要用于存放存活时间比较短的对象,新生代内存空间在64位和32位系统上默认为32MB和16MB。新生代的垃圾回收机制采用的的是Scavenge算法,Scavenge具体实现采用的是Cheney算法,Cheney将新生代的内存空间一分为二,每一部分空间称为semispace
两个semispace中只有一个处于使用中,另一个处于闲置状态,处于使用中的是From空间,处于闲置状态的是To空间
分配的对象首先是在From空间分配,当开始垃圾回收机制时,先检查From空间中存活的对象,然后将存活对象复制到To空间,非存活对象则被销毁,最后将From空间和To空间交换。
Scavenge算法的缺点是只能利用一半的内存空间,在生命周期短的场景下存活对象只占少部分,所以Scavenge算法拥有比较高的时间效率。这是典型的以空间换时间的做法。

2. 晋升

当一个对象经过多次复制任然存活时,它将被认为是生命周期比较长的对象,这种对象会被移动到老生代中,这个过程称为晋升
对象晋升的条件主要有两个:

  • 对象是否经历过Scavenge回收:默认情况下只要经历了一次新生代回收就会被移动到老生代;
  • To空间的内存占用率:如果To空间内存占用率超过25%,则对象会被移动到老生代空间(如果To空间内存占用比例较高会影响新对象的分配)。

3. 老生代

老生代主要采用了Mark-SweepMark-Compact相结合的方式进行垃圾回收。

  • Mark-Sweep:标记清除,分为标记清除两个阶段。标记阶段会遍历老生代中的所有对象,并标记存活的对象。清除阶段会清除没有被标记的对象。
  • Mark-Compact:标记整理,经过标记清除后内存会变得碎片化,Mark-Compact将存活的对象往一端移动,整个移动完成之后可以解决内存碎片化的问题。这个过程是很慢的。所以只有当老生代空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact。

4. 增量式标记

V8执行垃圾回收时需要暂停应用程序,为了降低全堆垃圾回收带来的停顿时间,V8在标记过程中采用了增量标记,每标记一段空间,就让应用程序执行一会儿,直到所有标记完成。
此外V8还引入了延迟清理增量式整理,让清理和整理过程也变成增量式的。

四、内存泄漏

通常造成内存泄漏的原因有:

  1. 缓存:使用内存作为缓存时,缓存始终保留在内存中越积越多会严重影响垃圾回收的性能,在Node中要谨慎使用缓存;
  2. 队列消费不及时:队列中的对象本来应该及时清理的,但是由于外部IO太慢等原因,队列任务被挤压,此时相应的对象就得不到释放。
  3. 作用域未释放:典型的场景就是闭包。