You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

13 KiB

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

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

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

典型回答

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

  • Serial GC它是最古老的垃圾收集器“Serial”体现在其收集工作是单线程的并且在进行垃圾收集过程中会进入臭名昭著的“Stop-The-World”状态。当然其单线程设计也意味着精简的GC实现无需维护复杂的数据结构初始化也简单所以一直是Client模式下JVM的默认选项。
    从年代的角度通常将其老年代实现单独称作Serial Old它采用了标记-整理Mark-Compact算法区别于新生代的复制算法。
    Serial GC的对应JVM参数是
-XX:+UseSerialGC

  • ParNew GC很明显是个新生代GC实现它实际是Serial GC的多线程版本最常见的应用场景是配合老年代的CMS GC工作下面是对应参数
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

  • CMSConcurrent Mark Sweep GC基于标记-清除Mark-Sweep算法设计目标是尽量减少停顿时间这一点对于Web等反应时间敏感的应用非常重要一直到今天仍然有很多系统使用CMS GC。但是CMS采用的标记-清除算法存在着内存碎片化问题所以难以避免在长时间运行等情况下发生full GC导致恶劣的停顿。另外既然强调了并发ConcurrentCMS会占用更多CPU资源并和用户线程争抢。

  • Parallel GC在早期JDK 8等版本中它是server模式JVM的默认GC选择也被称作是吞吐量优先的GC。它的算法和Serial GC比较相似尽管实现要复杂的多其特点是新生代和老年代GC都是并行进行的在常见的服务器环境中更加高效。
    开启选项是:

-XX:+UseParallelGC

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

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

  • G1 GC这是一种兼顾吞吐量和停顿时间的GC实现是Oracle JDK 9以后的默认GC选项。G1可以直观的设定停顿时间的目标相比于CMS GCG1未必能做到CMS在最好情况下的延时停顿但是最差情况要好很多。
    G1 GC仍然存在着年代的概念但是其内存结构并不是简单的条带式划分而是类似棋盘的一个个region。Region之间是复制算法但整体上实际可看作是标记-整理Mark-Compact算法可以有效地避免内存碎片尤其是当Java堆非常大的时候G1的优势更加明显。
    G1吞吐量和停顿表现都非常不错并且仍然在不断地完善与此同时CMS已经在JDK 9中被标记为废弃deprecated所以G1 GC值得你深入掌握。

考点分析

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

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

  • 垃圾收集的算法有哪些?如何判断一个对象是否可以回收?

  • 垃圾收集器工作的基本流程。

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

知识扩展

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

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

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

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

  • 引用计数算法顾名思义就是为对象添加一个引用计数用于记录对象被引用的情况如果计数为0即表示对象可回收。这是很多语言的资源回收选择例如因人工智能而更加火热的Python它更是同时支持引用计数和垃圾收集机制。具体哪种最优是要看场景的业界有大规模实践中仅保留引用计数机制以提高吞吐量的尝试。
    Java并没有选择引用计数是因为其存在一个基本的难题也就是很难处理循环引用关系。

  • 另外就是Java选择的可达性分析Java的各种引用关系在某种程度上将可达性问题还进一步复杂化具体请参考专栏第4讲,这种类型的垃圾收集通常叫作追踪性垃圾收集(Tracing Garbage Collection)。其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots然后跟踪引用链条如果一个对象和GC Roots之间不可达也就是不存在引用链条那么即可认为是可回收对象。JVM会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量作为GC Roots。

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

-XX:+ClassUnloadingWithConcurrentMark

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

  • 复制Copying算法我前面讲到的新生代GC基本都是基于复制算法过程就如专栏上一讲所介绍的将活着的对象复制到to区域拷贝过程中将对象顺序放置就可以避免内存碎片化。
    这么做的代价是既然要进行复制既要提前预留内存空间有一定的浪费另外对于G1这种分拆成为大量region的GC复制而不是移动意味着GC需要维护region之间对象引用关系这个开销也不小不管是内存占用或者时间开销。

  • 标记-清除Mark-Sweep算法首先进行标记工作标识出所有要回收的对象然后进行清除。这么做除了标记、清除过程效率有限另外就是不可避免的出现碎片化问题这就导致其不适合特别大的堆否则一旦出现Full GC暂停时间可能根本无法接受。

  • 标记-整理Mark-Compact类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。

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

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

垃圾收集过程的理解

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

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

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

第二, 经过一次Minor GCEden就会空闲下来直到再次达到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方式分别是

  • Epsilon GC简单说就是个不做垃圾收集的GC似乎有点奇怪有的情况下例如在进行性能测试的时候可能需要明确判断GC本身产生了多大的开销这就是其典型应用场景。

  • ZGC这是Oracle开源出来的一个超级GC实现具备令人惊讶的扩展能力比如支持T bytes级别的堆大小并且保证绝大部分情况下延迟都不会超过10 ms。虽然目前还处于实验阶段仅支持Linux 64位的平台但其已经表现出的能力和潜力都非常令人期待。

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

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

一课一练

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

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

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