第3章 垃圾收集器与内存分配策略6

3.7 选择合适的垃圾收集器

HotSpot虚拟机提供了种类繁多的垃圾收集器,选择太多反而令人踌躇难决,若只挑最先进的显然不可能满足全部应用场景,但只用一句“必须因地制宜,按需选用”又未免有敷衍的嫌疑,本节我们就来探讨一下如何选择合适的垃圾收集器。

3.7.1 Epsilon收集器

在G1、Shenandoah或者ZGC这些越来越复杂、越来越先进的垃圾收集器相继出现的同时,也有一个“反其道而行”的新垃圾收集器出现在JDK 11的特征清单中——Epsilon,这是一款以不能够进行垃圾收集为“卖点”的垃圾收集器,这种话听起来第一感觉就十分违反逻辑,这种“不干活”的收集器要它何用?

Epsilon收集器由RedHat公司在JEP 318中提出,在此提案里Epsilon被形容成一个无操作的收集器(A No-Op Garbage Collector),而事实上只要Java虚拟机能够工作,垃圾收集器便不可能是真正“无操作”的。原因是“垃圾收集器”这个名字并不能形容它全部的职责,更贴切的名字应该是本书为这一部分所取的标题——“自动内存管理子系统”。一个垃圾收集器除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责,其中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正常运作的必要支持,是一个最小化功能的垃圾收集器也必须实现的内容。从JDK10开始,为了隔离垃圾收集器与Java虚拟机解释、编译、监控等子系统的关系,RedHat提出了垃圾收集器的统一接口,即JEP 304提案,Epsilon是这个接口的有效性验证和参考实现,同时也用于需要剥离垃圾收集器影响的性能测试和压力测试。

在实际生产环境中,不能进行垃圾收集的Epsilon也仍有用武之地。很长一段时间以来,Java技术体系的发展重心都在面向长时间、大规模的企业级应用和服务端应用,尽管也有移动平台(指Java ME而不是Android)和桌面平台的支持,但使用热度上与前者相比要逊色不少。可是近年来大型系统从传统单体应用向微服务化、无服务化方向发展的趋势已越发明显,Java在这方面比起Golang等后起之秀来确实有一些先天不足,使用率正渐渐下降。传统Java有着内存占用较大,在容器中启动时间长,即时编译需要缓慢优化等特点,这对大型应用来说并不是什么太大的问题,但对短时间、小规模的服务形式就有诸多不适。为了应对新的技术潮流,最近几个版本的JDK逐渐加入了提前编译、面向应用的类数据共享等支持。Epsilon也是有着类似的目标,如果读者的应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择。

3.7.2 收集器的权衡

如果算上Epsilon,本书中已经介绍过十款HotSpot虚拟机的垃圾收集器了,此外还涉及Azul System公司的PGC、C4等收集器,再加上本章中并没有出现,但其实也颇为常用的OpenJ9中的垃圾收集器,把这些收集器罗列出来就仿佛是一幅琳琅画卷、一部垃圾收集的技术演进史。现在可能有读者要犯选择困难症了,我们应该如何选择一款适合自己应用的收集器呢?这个问题的答案主要受以下三个因素影响:

  • 应用程序的主要关注点是什么?如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点;如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
  • 运行应用的基础设施如何?譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是ARM/Aarch64;处理器的数量多少,分配内存的大小;选择的操作系统是Linux、Solaris还是Windows等
  • 使用JDK的发行商是什么?版本号是多少?是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9抑或是其他公司的发行版?该JDK对应了《Java虚拟机规范》的哪个版本?

一般来说,收集器的选择就从以上这几点出发来考虑。举个例子,假设某个直接面向用户提供服务的B/S系统准备选择垃圾收集器,一般来说延迟时间是这类应用的主要关注点,那么:

  • 如果你有充足的预算但没有太多调优经验,那么一套带商业技术支持的专有硬件或者软件解决方案是不错的选择,Azul公司以前主推的Vega系统和现在主推的ZingVM是这方面的代表,这样你就可以使用传说中的C4收集器了。
  • 如果你虽然没有足够预算去使用商业解决方案,但能够掌控软硬件型号,使用较新的版本,同时又特别注重延迟,那ZGC很值得尝试。
  • 如果你对还处于实验状态的收集器的稳定性有所顾虑,或者应用必须运行在Windows操作系统下,那ZGC就无缘了,试试Shenandoah吧。
  • 如果你接手的是遗留系统,软硬件基础设施和JDK版本都比较落后,那就根据内存规模衡量一下,对于大概4GB到6GB以下的堆内存,CMS一般能处理得比较好,而对于更大的堆内存,可重点考察一下G1。

当然,以上都是仅从理论出发的分析,实战中切不可纸上谈兵,根据系统实际情况去测试才是选择收集器的最终依据。

3.7.3 虚拟机及垃圾收集器日志

阅读分析虚拟机和垃圾收集器的日志是处理Java虚拟机内存问题必备的基础技能,垃圾收集器日志是一系列人为设定的规则,多少有点随开发者编码时的心情而定,没有任何的“业界标准”可言,换句话说,每个收集器的日志格式都可能不一样。除此以外还有一个麻烦,在JDK 9以前,HotSpot并没有提供统一的日志处理框架,虚拟机各个功能模块的日志开关分布在不同的参数上,日志级别、循环日志大小、输出格式、重定向等设置在不同功能上都要单独解决。直到JDK 9,这种混乱不堪的局面才终于消失,HotSpot所有功能的日志都收归到了“-Xlog”参数上,这个参数的能力也相应被极大拓展了:

-Xlog[:[selector][:[output][:[decorators][:output-potions]]]]

命令行中最关键的参数是选择器(Selector),它由标签(Tag)和日志级别(Level)共同组成。标签可理解为虚拟机中某个功能模块的名字,它告诉日志框架用户希望得到虚拟机哪些功能的日志输出。垃圾收集器的标签名称为“gc”,由此可见,垃圾收集器日志只是HotSpot众多功能日志的其中一项,全部支持的功能模块标签名如下所示:
在这里插入图片描述
日志级别从低到高,共有Trace,Debug,Info,Warning,Error,Off六种级别,日志级别决定了输出信息的详细程度,默认级别为Info,HotSpot的日志规则与Log4j、SLF4j这类Java日志框架大体上是一致的。另外,还可以使用修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加在日志行上的信息包括:

  • time:当前日期和时间。
  • uptime:虚拟机启动到现在经过的时间,以秒为单位。
  • timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
  • uptimemillis:虚拟机启动到现在经过的毫秒数。
  • timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
  • uptimenanos:虚拟机启动到现在经过的纳秒数。
  • pid:进程ID。
  • tid:线程ID。
  • level:日志级别。
  • tags:日志输出的标签集。

如果不指定,默认值是uptime、level、tags这三个,此时日志输出类似于以下形式:

[3.080s][info][gc,cpu] GC(5) User=0.03s Sys=0.00s Real=0.01s

举几个例子,展示在JDK 9统一日志框架后是如何获得垃圾收集器过程的相关信息,以下均以JDK 9的G1收集器(JDK 9下默认收集器就是G1,所以命令行中没有指定收集器)为例。

  1. 查看GC基本信息,在JDK 9之前使用 -XX:+PrintGC ,JDK 9后使用 -Xlog:gc
java -Xlog:gc GCTest.java
[0.034s][info][gc] Using G1
[1.005s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 12M->3M(256M) 11.058ms
  1. 查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc*,用通配符*将GC标签下所有细分过程都打印出来,如果把日志级别调整到Debug或者Trace(基于版面篇幅考虑,例子中并没有),还将获得更多细节信息:
>java -Xlog:gc* GCTest.java
[0.021s][info][gc,heap] Heap region size: 1M
[0.037s][info][gc     ] Using G1
[0.038s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
[1.044s][info][gc,start     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[1.045s][info][gc,task      ] GC(0) Using 2 workers of 2 for evacuation
[1.057s][info][gc,phases    ] GC(0)   Pre Evacuate Collection Set: 0.0ms
[1.058s][info][gc,phases    ] GC(0)   Evacuate Collection Set: 11.1ms
[1.058s][info][gc,phases    ] GC(0)   Post Evacuate Collection Set: 0.3ms
[1.059s][info][gc,phases    ] GC(0)   Other: 1.6ms
[1.059s][info][gc,heap      ] GC(0) Eden regions: 12->0(10)
[1.060s][info][gc,heap      ] GC(0) Survivor regions: 0->2(2)
[1.060s][info][gc,heap      ] GC(0) Old regions: 0->2
[1.061s][info][gc,heap      ] GC(0) Humongous regions: 0->0
[1.063s][info][gc,metaspace ] GC(0) Metaspace: 13653K->13653K(1062912K)
[1.064s][info][gc           ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 12M->3M(256M) 19.618ms
[1.066s][info][gc,cpu       ] GC(0) User=0.03s Sys=0.00s Real=0.03s
[1.362s][info][gc,heap,exit ] Heap
[1.363s][info][gc,heap,exit ]  garbage-first heap   total 262144K, used 7838K [0x0000000700000000, 0x0000000800000000)
[1.365s][info][gc,heap,exit ]   region size 1024K, 7 young (7168K), 2 survivors (2048K)
[1.366s][info][gc,heap,exit ]  Metaspace       used 14985K, capacity 15325K, committed 15616K, reserved 1062912K
[1.368s][info][gc,heap,exit ]   class space    used 1621K, capacity 1752K, committed 1792K, reserved 1048576K
  1. 查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之后使用-Xlog:gc+heap=debug
>java -Xlog:gc+heap=debug GCTest.java
[0.015s][info][gc,heap] Heap region size: 1M
[0.016s][debug][gc,heap] Minimum heap 8388608  Initial heap 268435456  Maximum heap 4294967296
[0.928s][debug][gc,heap] GC(0) Heap before GC invocations=0 (full 0): garbage-first heap   total 262144K, used 11264K [0x0000000700000000, 0x0000000800000000)
[0.930s][debug][gc,heap] GC(0)   region size 1024K, 12 young (12288K), 0 survivors (0K)
[0.932s][debug][gc,heap] GC(0)  Metaspace       used 13659K, capacity 13973K, committed 14208K, reserved 1062912K
[0.933s][debug][gc,heap] GC(0)   class space    used 1464K, capacity 1578K, committed 1664K, reserved 1048576K
[0.951s][info ][gc,heap] GC(0) Eden regions: 12->0(10)
[0.952s][info ][gc,heap] GC(0) Survivor regions: 0->2(2)
[0.954s][info ][gc,heap] GC(0) Old regions: 0->2
[0.955s][info ][gc,heap] GC(0) Humongous regions: 0->0
[0.958s][debug][gc,heap] GC(0) Heap after GC invocations=1 (full 0): garbage-first heap   total 262144K, used 3765K [0x0000000700000000, 0x0000000800000000)
[0.960s][debug][gc,heap] GC(0)   region size 1024K, 2 young (2048K), 2 survivors (2048K)
[0.961s][debug][gc,heap] GC(0)  Metaspace       used 13659K, capacity 13973K, committed 14208K, reserved 1062912K
[0.962s][debug][gc,heap] GC(0)   class space    used 1464K, capacity 1578K, committed 1664K, reserved 1048576K
  1. 查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用 -XX:+Print-GCApplicationConcurrentTime以及 -XX:+PrintGCApplicationStoppedTime ,JDK 9之后使用 -Xlog:safepoint
>java -Xlog:safepoint GCTest.java
[0.238s][info][safepoint] Entering safepoint region: EnableBiasedLocking
[0.239s][info][safepoint] Leaving safepoint region
[0.239s][info][safepoint] Total time for which application threads were stopped: 0.0014763 seconds, Stopping threads took: 0.0009254 seconds
[0.697s][info][safepoint] Application time: 0.4549877 seconds
[0.698s][info][safepoint] Entering safepoint region: Deoptimize
[0.701s][info][safepoint] Leaving safepoint region
[0.702s][info][safepoint] Total time for which application threads were stopped: 0.0034496 seconds, Stopping threads took: 0.0019594 seconds
[0.946s][info][safepoint] Application time: 0.2419424 seconds
[0.947s][info][safepoint] Entering safepoint region: G1CollectForAllocation
[0.961s][info][safepoint] Leaving safepoint region
[0.962s][info][safepoint] Total time for which application threads were stopped: 0.0153465 seconds, Stopping threads took: 0.0016478 seconds
[1.120s][info][safepoint] Application time: 0.1550772 seconds
[1.121s][info][safepoint] Entering safepoint region: Deoptimize
[1.123s][info][safepoint] Leaving safepoint region
[1.124s][info][safepoint] Total time for which application threads were stopped: 0.0031163 seconds, Stopping threads took: 0.0016547 seconds
[1.196s][info][safepoint] Application time: 0.0698429 seconds
[1.197s][info][safepoint] Entering safepoint region: Halt
  1. 查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace
>java -Xlog:gc+ergo*=trace GCTeset.java
[0.017s][debug][gc,ergo,heap] Expand the heap. requested expansion amount: 268435456B expansion amount: 268435456B
[0.031s][debug][gc,ergo,refine] Initial Refinement Zones: green: 2, yellow: 6, red: 10, min yellow size: 4
[0.909s][trace][gc,ergo,cset  ] GC(0) Start choosing CSet. pending cards: 0 predicted base time: 10.00ms remaining time: 190.00ms target pause time: 200.00ms
[0.911s][trace][gc,ergo,cset  ] GC(0) Add young regions to CSet. eden: 12 regions, survivors: 0 regions, predicted young region time: 608.78ms, target pause time: 200.00ms
[0.913s][debug][gc,ergo,cset  ] GC(0) Finish choosing CSet. old: 0 regions, predicted old region time:0.00ms, time remaining: 0.00
[0.930s][debug][gc,ergo       ] GC(0) Running G1 Clear Card Table Task using 1 workers for 1 units of work for 12 regions.
[0.933s][debug][gc,ergo       ] GC(0) Running G1 Free Collection Set using 1 workers for collection set length 12
[0.934s][trace][gc,ergo,refine] GC(0) Updating Refinement Zones: update_rs time: 0.001ms, update_rs buffers: 0, update_rs goal time: 19.998ms
[0.936s][debug][gc,ergo,refine] GC(0) Updated Refinement Zones: green: 2, yellow: 6, red: 10
  1. 查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution,JDK 9之后使用-Xlog:gc+age=trace
>java -Xlog:gc+age=trace GCTest.java
[0.909s][debug][gc,age] GC(0) Desired survivor size 1048576 bytes, new threshold 15 (max threshold 15)
[0.921s][trace][gc,age] GC(0) Age table with threshold 15 (max threshold 15)
[0.922s][trace][gc,age] GC(0) - age   1:    2092336 bytes,    2092336 total

囿于篇幅原因,不再一一列举,下图给出了全部在JDK 9中被废弃的日志相关参数及它们在JDK 9后使用-Xlog的代替配置形式。
 JDK 9前后日志参数变化

3.7.4 垃圾收集器参数总结

HotSpot虚拟机中的各种垃圾收集器到此全部介绍完毕,在描述过程中提到了很多虚拟机非稳定的运行参数,下面整理了这些参数,供读者实践时参考。
垃圾收集相关的常用参数
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值