19. Go 对比 Java、V8 中 JavaScript 的 GC 性能如何?

无论是 Java 还是 JavaScript 中的 GC 均为分代式 GC。分代式 GC 的一个核心假设就是分代假说:将对象依据存活时间分配到不同的区域,每次回收只回收其中的一个区域。

V8 的 GC

在 V8 中主要将内存分为新生代和老生代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长、常驻内存、占用内存较大的对象:

  1. 新生代中的对象主要通过副垃圾回收器进行回收。该回收过程是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,这两个空间中只有一个处于使用中,另一个则处于闲置状态。处于使用状态的空间称为 From 空间,处于闲置的空间称为 To 空间。分配对象时,先是在 From 空间中进行分配,当开始垃圾回收时,会检查 From 空间中的存活对象,并将这些存活对象复制到 To 空间中,而非存活对象占用的空间被释放。完成复制后,From 空间和 To 空间的角色互换。也就是通过将存活对象在两个空间中进行复制。
  2. 老生代则由主垃圾回收器负责。它实现的是标记清扫过程,但略有不同之处在于它还会在清扫完成后对内存碎片进行整理,进而是一种标记整理的回收器。

Java 的 GC

Java 的 GC 称之为 G1,并将整个堆分为年轻代、老年代和永久代。包括四种不同的收集操作,从上往下的这几个阶段会选择性地执行,触发条件是用户的配置和实际代码行为的预测。

  1. 年轻代收集周期:只对年轻代对象进行收集与清理
  2. 老年代收集周期:只对老年代对象进行收集与清理
  3. 混合式收集周期:同时对年轻代和老年代进行收集与清理
  4. 完整 GC 周期:完整的对整个堆进行收集与清理

在回收过程中,G1 会对停顿时间进行预测,竭尽所能地调整 GC 的策略从而达到用户代码通过系统参数(-XX:MaxGCPauseMillis)所配置的对停顿时间的要求。

这四个周期的执行成本逐渐上升,优化得当的程序可以完全避免完整 GC 周期。

性能比较

在 Go、Java 和 V8 JavaScript 之间比较 GC 的性能本质上是一个不切实际的问题。如前面所说,垃圾回收器的设计权衡了很多方面的因素,同时还受语言自身设计的影响,因为语言的设计也直接影响了程序员编写代码的形式,也就自然影响了产生垃圾的方式。

但总的来说,他们三者对垃圾回收的实现都需要 STW,并均已达到了用户代码几乎无法感知到的状态(据 Go GC 作者 Austin 宣称STW 小于 100 微秒 [14])。当然,随着 STW 的减少,垃圾回收器会增加 CPU 的使用率,这也是程序员在编写代码时需要手动进行优化的部分,即充分考虑内存分配的必要性,减少过多申请内存带给垃圾回收器的压力。