第27讲|Java常见的垃圾收集器有哪些?

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展, Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

今天我要问你的问题是,Java常见的垃圾收集器有哪些?

典型回答

实际上,垃圾收集器(GC,Garbage Collector)是和具体JVM实现紧密相关的,不同厂商(IBM、Oracle),不同版本的JVM,提供的选择也不同。接下来,我来谈谈最主流的Oracle JDK。

-XX:+UseSerialGC
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseParallelGC

另外,Parallel GC引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标,JVM会自动进行适应性调整,例如下面参数:

-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC时间和用户时间比例 = 1 / (N+1)

考点分析

今天的问题是考察你对GC的了解,GC是Java程序员的面试常见题目,但是并不是每个人都有机会或者必要对JVM、GC进行深入了解,我前面的总结是为不熟悉这部分内容的同学提供一个整体的印象。

对于垃圾收集,面试官可以循序渐进从理论、实践各种角度深入,也未必是要求面试者什么都懂。但如果你懂得原理,一定会成为面试中的加分项。在今天的讲解中,我侧重介绍比较通用、基础性的部分:

另外,Java一直处于非常迅速的发展之中,在最新的JDK实现中,还有多种新的GC,我会在最后补充,除了前面提到的垃圾收集器,看看还有哪些值得关注的选择。

知识扩展

垃圾收集的原理和基础概念

第一,自动垃圾收集的前提是清楚哪些内存可以被释放。这一点可以结合我前面对Java类加载和内存结构的分析,来思考一下。

主要就是两个方面,最主要部分就是对象实例,都是存储在堆上的;还有就是方法区中的元数据等信息,例如类型不再使用,卸载该Java类似乎是很合理的。

对于对象实例收集,主要是两种基本算法,引用计数和可达性分析。

方法区无用元数据的回收比较复杂,我简单梳理一下。还记得我对类加载器的分类吧,一般来说初始化类加载器加载的类型是不会进行类卸载(unload)的;而普通的类型的卸载,往往是要求相应自定义类加载器本身被回收,所以大量使用动态类型的场合,需要防止元数据区(或者早期的永久代)不会OOM。在8u40以后的JDK中,下面参数已经是默认的:

-XX:+ClassUnloadingWithConcurrentMark

第二,常见的垃圾收集算法,我认为总体上有个了解,理解相应的原理和优缺点,就已经足够了,其主要分为三类:

注意,这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。

如果对这方面的算法有兴趣,可以参考一本比较有意思的书《垃圾回收的算法与实现》,虽然其内容并不是围绕Java垃圾收集,但是对通用算法讲解比较形象。

垃圾收集过程的理解

我在专栏上一讲对堆结构进行了比较详细的划分,在垃圾收集的过程,对应到Eden、Survivor、Tenured等区域会发生什么变化呢?

这实际上取决于具体的GC方式,先来熟悉一下通常的垃圾收集流程,我画了一系列示意图,希望能有助于你理解清楚这个过程。

第一,Java应用不断创建对象,通常都是分配在Eden区域,当其空间占用达到一定阈值时,触发minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到JVM选择的Survivor区域,而没有被引用的对象(黄色方块)则被回收。注意,我给存活对象标记了“数字1”,这是为了表明对象的存活时间。

第二, 经过一次Minor GC,Eden就会空闲下来,直到再次达到Minor GC触发条件,这时候,另外一个Survivor区域则会成为to区域,Eden区域的存活对象和From区域对象,都会被复制到to区域,并且存活的年龄计数会被加1。

第三, 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,如下图所示,超过阈值的对象会被晋升到老年代。这个阈值是可以通过参数指定:

-XX:MaxTenuringThreshold=<N>

后面就是老年代GC,具体取决于选择的GC选项,对应不同的算法。下面是一个简单标记-整理算法过程示意图,老年代中的无用对象被清除后, GC会将对象进行整理,以防止内存碎片化。

通常我们把老年代GC叫作Major GC,将对整个堆进行的清理叫作Full GC,但是这个也没有那么绝对,因为不同的老年代GC算法其实表现差异很大,例如CMS,“concurrent”就体现在清理工作是与工作线程一起并发运行的。

GC的新发展

GC仍然处于飞速发展之中,目前的默认选项G1 GC在不断的进行改进,很多我们原来认为的缺点,例如串行的Full GC、Card Table扫描的低效等,都已经被大幅改进,例如, JDK 10以后,Full GC已经是并行运行,在很多场景下,其表现还略优于Parallel GC的并行Full GC实现。

即使是Serial GC,虽然比较古老,但是简单的设计和实现未必就是过时的,它本身的开销,不管是GC相关数据结构的开销,还是线程的开销,都是非常小的,所以随着云计算的兴起,在Serverless等新的应用场景下,Serial GC找到了新的舞台。

比较不幸的是CMS GC,因为其算法的理论缺陷等原因,虽然现在还有非常大的用户群体,但是已经被标记为废弃,如果没有组织主动承担CMS的维护,很有可能会在未来版本移除。

如果你有关注目前尚处于开发中的JDK 11,你会发现,JDK又增加了两种全新的GC方式,分别是:

当然,其他厂商也提供了各种独具一格的GC实现,例如比较有名的低延迟GC,ZingShenandoah等,有兴趣请参考我提供的链接。

今天,作为GC系列的第一讲,我从整体上梳理了目前的主流GC实现,包括基本原理和算法,并结合我前面介绍过的内存结构,对简要的垃圾收集过程进行了介绍,希望能够对你的相关实践有所帮助。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天谈了一堆的理论,思考一个实践中的问题,你通常使用什么参数去打开GC日志呢?还会额外添加哪些选项?

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。