Java垃圾回收精粹 — Part4

时间:2023-01-21 15:44:32

Java垃圾回收精粹分4个部分,本篇是第4部分。在第4部分里介绍了G1收集器、其他并发收集器以及垃圾收集监控和调优。

Garbage First (G1) 收集器

G1 (-XX:+UseG1GC)收集器是一个新的收集器。G1随Java 6发布,在Java 7U4中得到正式支持。它是一个部分并发的收集算法,通过尝试小量增加全局暂停的方式压缩年老区,将FullGC降到最低。因为碎片引起的FullGC正是CMS的一大麻烦。G1也是分代收集器,但是它与其他收集器使用不同的堆组织方式。根据不同的用途,G1将堆划分为一大批(约2000个)固定大小的区,而不是相同用途的堆连续在一起。

Java垃圾回收精粹 — Part4

G1并发地标记区域以跟踪区域之间的引用,同时关注收集区域中的最大空闲空间。这些区域在递增的全局暂停中被收集,存活的对象被剪切到一个空的区域里面,这样整个过程就是压缩的。在同一个周期里收集的区域叫做Collection Set。

译注:G1会跟踪各个区域中的垃圾堆积的价值大小、回收后获得的空间大小以及回收所需时间,在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的区域,也就是Garbage-First名称的来由。

超过区域大小50%的对象会在大区域里分配,其大小可以达到当前区域大小的数倍大。G1收集和分配大对象操作的开销非常大,更加悲剧的是,目前还没有任何优化措施。

任何压缩收集器所遇到的挑战不是去移动对象,而是更新这些对象的引用。如果一个对象被许多个区域引用,那么更新这些引用肯定会比移动对象更加耗时。G1通过“记录集(Remembered Sets)” 跟踪区域中被其他区域引用的那些对象。记忆集是一些卡片的集合,这些卡片上标记着更新信息。如果记忆集变大,那么G1就会显著变慢。当从一个区域转移对象到另一区域时,由此引发的全局暂停时间与需要扫描和更新引用区域的数量成正比。

维护记录集会增加次要回收的成本,结果导致花费的时间比并行旧生代收集器和CMS收集器中次要回收暂停的时间更久。

G1是目标驱动的,可以通过“–XX:MaxGCPauseMillis=<n>”设置延迟时间,默认是200ms。该参数只会尽可能影响每个周期的工作量,但是不保证最终效果。设置为几十毫秒大多是徒劳的,而且几十毫秒也不是G1关注的目标。

如果你的应用程序可以容忍暂停0.5-1.0秒的增量压缩时间,而且此应用拥有一个会逐渐碎片化的堆,那么G1会是通用的收集器的一个很好选择。最坏情况是碎片引起的暂停,我们之前在CMS那儿也见过。G1 倾向于减少这种暂停的频率,因为那会花费额外的次要回收和对年老代的增量压缩。大部分的暂停被限制在区域层面而不是整个堆的压缩。

与CMS一样,G1也会因为无法保证晋升率而失败,最终求助于全局暂停的FullGC。就像CMS存在“并发模式失败”一样,G1也可能遭遇转移失败,在日志中看到“到达空间溢出(to-space overflow)”。没有空余区域可供对象转移进去,跟晋升失败类似。如果发生这种情况,试试使用更大的堆、更多的标记线程,但在某些情况下,需要应用程序作出改变以减少分配比率。

G1的一个有挑战性的问题是,如何处理好高关注率的对象和区域。 当区域里的存活对象没有被其他区域大量引用时,增量的全局暂停进行压缩会很高效。如果一个对象或者区域被大量引用了,记录集将会相应地变大,并且G1会避免收集这些对象。最终没有办法,只能频繁地使用中等长度的暂停时间来压缩堆大小。

其他并发收集器

CMS和G1通常被称为并发性最好的收集器。然而当你观察整个工作过程,很显然地,新生代、晋升、甚至多数年老代的工作根本不是并发的。CMS对年老代来说是并发性最好的算法,G1更像是全局暂停的增量收集器。CMS和G1都明显地拥有规律的全局暂停发生,并且在最坏情况下他们不适合严格的低延迟应用,比如金融交易或用户交互界面。

其他可用的的收集器还有:Oracle JRockit Real Time、IBM WebSphere Real Time、 Azul Zing。JRockit和Websphere收集器大多数情况下在延迟上控制得比CMS和G1好,但是在大多数情况下它们有吞吐量的限制,并且会有明显的全局暂停。Zing是洒家知道的一款能对所有代都真正并发收集和压缩,同时保持了高吞吐率的Java收集器。Zing确实有一些亚毫秒级的全局暂停,但这些是在与存活对象集大小无关的收集周期里完成。

JRockit Real Time在堆大小合适时,可以将暂停时间控制在几十毫秒,但是偶尔也会失败转为完全压缩暂停。WebSphere RealTime通过约束分配率和存活集的大小,可以将暂停时间控制在几毫秒。Zing在所有阶段并发以保证高分配率,从而实现亚毫秒级的暂停,包括次要收集阶段。无论堆大小,Zing都能够保持行为的一致性。如果需要保证程序的吞吐量或者需要控制对象模型状态,用户完全可以添加更大的堆而不用担心增加暂停时间。

对于所有的并发收集器来说,如果你关注延迟,就必须牺牲吞吐量换取空间。依据并发收集器的效率,你可能放弃一点点的吞吐量,但是总能显著地增加空间。如果真正实现了并发很少会发生全局暂停,但是这就需要更多CPU内核来支持并发操作和维持吞吐量。

注意:当分配的空间足够时,所有的并发收集器倾向于更高效地运行。第一条经验,你应该预算至少两到三倍存活集大小以确保高效地操作。然而,大量并发操作所需的空间随着应用程序的吞吐量以及与之相关的对象分配和晋升率增长而增加。因此,对于高吞吐量的应用,维持较高的存活集的堆大小比率很有必要。鉴于今天的服务器拥有巨大的内存空间,这些不成问题。

垃圾收集监控和调优

要理解你的应用程序和垃圾收集器是如何工作的,启动JVM时要至少添加以下参数:

1
2
3
4
5
6
7
-verbose:gc
-Xloggc:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime

然后加载日志到像Chewiebug这样的工具进行分析。

为了看到GC动态运行,请启动JVisualVM并且安装Visual GC插件。接下来你就能看见你的应用程序的GC,行为如下图:

Java垃圾回收精粹 — Part4
要理解应用堆GC的要求,你需要一个有代表性且可以重复执行的负载测试。随着你堆每个收集器如何工作的理解,通过不同的配置运行负载测试,直至达到你期望的吞吐量和延迟。从最终用户的视角来看,测量延迟是最重要的。可以通过捕获每个测试请求的响应时间,将之记在直方图上,比如HdrHistogram或者Disruptor Histogram,这样你就能分析出更多东西。如果延迟峰值超出可接受范围,可以尝试关联GC日志来判断是否是GC出了问题。还有可能是其他问题导致的延迟高峰。另一个值得考虑的工具是jHiccup,可以用它来跟踪JVM暂停并且可以和系统合为一个整体。使用jHiccup测量空闲系统几个小时,通常会让你得到一个令人惊讶的结果。

如果延迟峰值由GC导致,那么你可以看看CMS或G1看是否可满足这个延迟目标。有时这是不可能的,因为高分配率和晋升率与低时延的要求是冲突的。GC调优是一个高技巧性的锻炼,常常需要修改程序以减少对象分配率或对象生命周期。如果需要权衡时间、GC资源优化和应用程序的修改及精通,那么可能必需购买商业的并发压缩JVM,比如JRockit Real Time 和 Azul Zing。

原文链接: mechanical-sympathy 翻译: ImportNew.com邢 敏
译文链接: http://www.importnew.com/8352.html