经历了数千次改进,Java 的垃圾回收在吞吐量、延迟和内存大小方面有了巨大的进步。
2014 年3 月 JDK 8 发布,自那以来 JDK 又连续发布了许多版本,直到今日的 JDK 18 是 Java 的第十个版本。借此机会,我们来回顾一下 HotSpot JVM 的垃圾回收器的发展全过程。
1. 关于垃圾回收、度量和取舍
HotSpot JVM 中负责管理应用程序堆的组件叫做“垃圾回收器”(Garbage Collector,即GC)。GC 负责管理应用程序堆对象的整个生命周期,从应用程序分配内存到内存被回收,都由 GC 负责。
从高层来看,JVM 垃圾回收算法的最基本功能如下:
-
当应用程序请求分配内存时,GC 负责提供内存。提供内存的过程应尽可能快;
-
GC 检测应用程序不再使用的内存。这个操作也应当十分高效,不应消耗太多时间。这种不再使用的内存称为“垃圾”;
-
GC 将同一块内存再次提供给应用程序,最好是“实时”,也就是要快。
好的垃圾回收算法还有更多的需求,但这三条是最基本的,也足以支撑本文的讨论了。
满足这些需求有很多方法,但很不幸,我们并没有一蹴而就的方法,也没有能一次性解决所有需求的方法。因此,JDK 提供了多种垃圾回收算法以供选择,每种算法适用于不同的场景。这些算法的实现基本上可以根据吞吐量、延迟和内存大小这三个性能度量,以及对应用程序的影响进行归类。
-
吞吐量指的是单位时间内能够完成的工作量。在此语境下,垃圾回收算法的优劣取决于能在单位时间内完成的回收工作量,这些算法可以让 Java 应用程序实现更高的吞吐量;
-
延迟指的是单次操作所需时间。垃圾回收算法需要尽可能减小延迟。在垃圾回收的语境下,关键点就是垃圾回收期是否会导致暂停、暂停的范围,以及暂停的时长;
-
在垃圾回收的语境下,内存大小指的是为了让垃圾回收期正常工作,需要在正常的应用程序堆内存之外,再额外占用多少内存。如果 GC(或更一般地,JVM)需要的内存很少,就可以给应用程序堆留出更多内存。
这三个度量是互相关联的:高吞吐量的垃圾回收器可能会严重影响延迟(但对应用程序的影响最小)。为了降低内存消耗,我们需要采用在其他度量方面不是那么出色的算法。延迟较低的回收期需要并行进行更多工作,或以更小的单位进行工作,这就会消耗更多处理器资源。
这些关系通常可以画成一个三角形,如图1所示。每个垃圾回收算法占据三角形的一个角。
图1. GC 性能度量三角
提高 GC 在某方面的表现,通常会导致其他方面的表现降低。
2. JDK 18 中的 OpenJDK GC
OpenJDK 提供了五种 GC,分别专注于不同的性能度量。表 1 列出了 GC 的名称、专注领域,以及实现特定特性所使用的核心概念。
表1. OpenJDK的五种GC
Parallel GC 是 JDK 8 以及更早版本的默认回收期。它专注于吞吐量,尽快完成工作,而很少考虑延迟(暂停)。
Parallel GC 会在 STW(全局暂停)期间,以更紧凑的方式,将正在使用中的内存移动(复制)到堆中的其他位置,从而制造出大片的空闲内存区域。当内存分配请求无法满足时就会发生 STW 暂停,然后JVM完全停止应用程序运行,投入尽可能多的处理器线程,让垃圾回收算法执行内存压缩工作,然后分配请求的内存,最后恢复应用程序执行。
Parallel GC 也是一个分代回收器,旨在最大化垃圾回收效率。本文稍后会详细讨论分代式回收的思想。
G1 GC 是JDK 9 以后的默认回收期。G1 试图平衡吞吐量和延迟。一方面,在 STW 暂停期间,依然会利用分代继续执行内存回收工作,从而最大化效率&