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