深入浅出Electron:原理、工程与实践
上QQ阅读APP看书,第一时间看更新

5.3 V8垃圾收集原理

V8的垃圾回收机制是一种称之为“代回收”的垃圾回收机制,它将内存分为两个生代:新生代和老生代。默认情况下,32位系统新生代内存大小为16MB,老生代内存大小为700MB,64位系统新生代内存大小为32MB,老生代内存大小为1.4GB。

新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就可以了,当存储空间快要满时,就进行一次垃圾回收。下面举例说明新生代是如何进行垃圾回收的。

首先V8把新生代内存平均分成两块相等的空间,一块叫作From,一块叫作To,当JavaScript运行时,创建的对象首先在From空间中分配内存,我们假设创建了3个对象:x、y和z,当垃圾回收时遍历这些对象,判断其是否存在引用,假设y对象不存在任何引用,x和z依然存在引用。

此时垃圾回收器将活跃对象x和z从From空间复制到To空间,之后清空From空间的全部内存,接着交换From空间和To空间的内容。

如你所见这个垃圾回收算法的弊端是只能使用新生代存储空间的一半,但由于这个算法使用了大量的指针操作和批量处理内存操作,使得这个算法效率非常高,这是一个典型的牺牲空间换取时间的算法。

当一个对象经过多次复制仍然存活时(或新生代空间使用超限时),它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。

当对老生代的对象执行垃圾回收逻辑时,V8遍历老生代的对象,判断其是否存在引用,如果存在引用,则做好标记,遍历完成后把没有标记的对象清除掉。这就是老生代的标记清除垃圾回收算法,此算法相较于新生代的垃圾回收算法来说效率要低得多。

无论是新生代的垃圾收集还是老生代的垃圾收集,都会判断对象是否存在引用,这个工作是递归的遍历根对象上的所有属性以及属性的子属性,看是否能访问到这个对象,如果能访问到,则说明这个对象还存在引用,如果不能访问到,则说明这个对象可以被垃圾收集。JavaScript有三种类型的根对象:

  • 全局的window对象(位于每个iframe中,Node.js中是global对象)。
  • 文档DOM树,由可以通过遍历文档到达的所有原生DOM节点组成。
  • 存放在栈上的变量,位于正在执行的函数内。

在Node.js环境中,可以通过如下代码来查看内存分配情况:

process.memoryUsage();
/* 返回值
{
  rss: 22638592,
  heapTotal: 6574080,
  heapUsed: 4499024,
  external: 901302,
  arrayBuffers: 27542
}
*/

这个方法返回一个对象,这个对象包含以下属性(以下所有内存单位均为字节(Byte))。

  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:V8引擎可以分配的最大堆内存,包含下面的heapUsed。
  • heapUsed:V8引擎已经分配使用的堆内存。
  • external:V8引擎管理C++对象绑定到JavaScript对象上的内存。

开发者可以在主进程启动之初、app ready之前,使用如下代码来扩大Node.js的堆内存:

app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096')

上述代码为Node.js的V8引擎设置了旧内存最大内存大小(4GB),当应用程序内存消耗接近极限时,V8将在垃圾回收上花费更多时间,以释放未使用的内存。如果堆内存消耗超出了限制,则会导致进程崩溃,因此这个值不应该设置得太低。当然,如果将它设置得太高,则V8允许的额外堆使用量可能会导致整个系统内存不足。在一台具有2GB内存的机器上,我可能会设置--max-old-space-size为1.5GB左右,以便为其他用途留一些内存并避免内存数据到磁盘的交换。