乔鑫 李志远 程建军
(浪潮电子信息产业股份有限公司 山东省济南市 250000)
垃圾回收(Garbage Collection, GC)是Java 虚拟机(JVM)中使用的一种内存管理方案,它能够不断自动释放内存中的不再被使用的对象,并按照特定的垃圾回收算法实现对于内存资源的管理。相较于C/C++手动内存管理方式,GC 的出现大大减少了开发人员在内存资源管理方面的工作,是JVM 的核心组成部分,并且对JVM 的性能有着重要的影响[1]。
随着技术及硬件的发展,新的垃圾回收方式也在随着JDK 版本的更新而被加入,如在JDK1.7 中正式引入了G1 回收器,JDK 11 中引入了Z 回收器等。面对如此多的垃圾回收器,如何在不同实际应用场景下中选择最适合的是进行JVM性能优化的首要问题,解决这一问题则必须先了解不同垃圾回收方式的性能表现。以往也有相关方面的研究,但性能测试或基于SPEC JVM2008[2]又或是基于SPEC jbb2000[3-4]等一些现已不再提供支持的测试工具,已不能适应当前时代的需求,而通过一种权威且最新的基准测试工具分析不同垃圾回收器的性能表现是十分有意义的。
SPEC jbb(Java Business Benchmark)基准测试是标准性能评估组织SPEC 发布的一项用于衡量服务器Java 应用性能的测试基准。从2000 年6 月SPEC 组织发布第一版服务器Java 性能测试基准 SPEC jbb2000 至今,历经了SPEC jbb2005(2006.1-2013.10)、SPEC jbb2013(2013.1-2014.12)多个版本的更新后,SPEC jbb2015(2015.9-至今)基于最新的Java 应用程序特性开发,支持虚拟化和云环境。现在SPEC jbb 测试已经发展成为JVM 厂商、Java 开发者,研究学者,以及相关学术机构评估Java 业务应用性能及可扩展性的一项权威基准测试标准[5]。
本文将首先从垃圾回收原理出发,分别介绍几种垃圾回收器,然后对这几种垃圾回收器在基准测试中的性能表现进行分析。
Java 自动化的管理内存资源必须通过垃圾回收算法来确定哪些是有效的对象,哪些是无效的对象,对于无效的对象就要进行垃圾回收处理。作为垃圾回收器的实现基础,下面先介绍常见的垃圾回收算法。
图1:几种垃圾回收器SPEC jbb2015 测试结果
图2:几种垃圾回收器平均停顿时间比较
在垃圾回收算法中,根是指向对象的指针的起点部分。通过根对象进行引用搜索,最终可达的对象被称为可达对象;通过根对象进行引用搜索,最终没有被引用的对象被称为不可达对象。在标记阶段,首先通从根节点开始标记所有的可达对象,不可达对象则为垃圾对象。在清除阶段,清除所有未被标记的对象。由于清除时不考虑内存空间的连续性,因此标记清除法最大的问题是产生内存空间碎片。特别是在进行大对象内存分配时,相较于连续内存空间,内存碎片的存在会降低堆内存效率。
复制算法将内存空间划分为相等的两部分,且每次只会只使用其中的一部分,当垃圾回收时,首先将使用中那部分内存里存活的对象复制到另一部分内存区,接着清除使用的内存块中的所有对象,最后将两个内存区进行互换,从而完成垃圾回收。复制算法在复制的过程中能够有效避免内存空间碎片的产生,但代价是系统内存空间损失一半。
标记压缩法是一种针对老年代的垃圾回收算法。对于老年代,由于大部分对象都是存活对象,使用复制算法代价将使内存折半,成本太高并不适用。标记压缩法正是为了应对这种情况而产生,首先从根节点开始,对所有可达对象进行标记,然后将所有存活的对象压缩到内存一端,再清理剩余的所有的空间。这种方法既避免了碎片的产生,又不需要将内存分为两半,效率很高。
分区算法将整个堆空间划分成连续的不同小区间,每一小区间都独立使用,独立回收,并可以控制一次回收的小区间数量。一般来说,在相同条件下,堆空间越大,进行一次垃圾回收的所需的时间就越长,导致产生的停顿时间也越长。因此如果将堆内存分割成多个小块,并根据目标停顿时间的要求,每次合理地控制回收部分小区间,而不是回收整个堆空间,则可以有效减小一次垃圾回收所产生的的停顿时间[6]。
垃圾收集算法可以看作Java 虚拟机内存回收的抽象策略,而垃圾收集器则是其内存回收的具体实现。Java 虚拟机中,垃圾回收器不只一种,在不同的应用场景下如何选择性能最佳的垃圾回收器是需要有清楚认识的,下面介绍几种常见的垃圾回收器。
串行回收器是一种单线程垃圾回收器,在进行每次垃圾回收时,只有一个线程工作,Java 应用程序中的其他所有线程暂停,等待垃圾回收完成(“Stop-The-World”过程)。在实时性要求高的应用场景下,往往会造成用户体验不佳。串行回收器在新生代中使用复制算法,老年代中使用标记压缩法。
并行回收器在串行回收器的基础上做了改进,它使用多个线程同时进行垃圾回收,可以有效减少垃圾回收所需的时间。新生代中使用复制算法,老年代使用标记压缩算法。并行回收器关注系统的吞吐量,可以通过-XX:MaxGCPauseMills 和-XX:GCTimeRatio 控制的垃圾回收最大停顿时间和吞吐量。但须指出的是,减少一次收集的最大停顿时间,就会同时减少系统吞吐量,增大系统吞吐量又可能会同时增加一次垃圾回收的最大停顿时间。
CMS 回收器使用标记清除算法,利用多线程并行回收,主要侧重于系统停顿时间。CMS 工作时首先经过初始标记与并发标记来标记出需要回收的对象、通过预清理做清理前的准备及控制停顿时间、再经过重新标记修正并发标记的数据、最后进行并发清除,以及并发重置为下次回收做准备[7]。
G1 回收器可以视为CMS 回收器的替代品,它使用了独特的分区算法,相比于之前介绍的垃圾回收器将堆内存划分为固定内存大小的年轻代、老代和永久代(JDK1.8 后被元空间取代),G1 回收器将堆分割成一组大小相等的区域,每个区域是一个连续的虚拟内存范围,某些区域被功能与年轻代老年代相同,但是它们没有固定的大小,这为内存使用提供了更大的灵活性。在进行垃圾回收时,G1 回收器可以只选择部分区域,且部分垃圾回收工作能与Java 应用程序并行,提高回收效率的同时相应降低停顿时间。G1 回收器虽然也使用标记清除法,但与CMS 不同的是G1 可以有效复制移动对象,消除了潜在的内存碎片问题。此外,G1 还允许用户自行设定所需的暂停时间[8]。
ZGC 是从JDK11 中引入的一种新的支持弹性伸缩的低延迟垃圾收集器,主要实现了三大目标:停顿时间不超过10ms、停顿时间不随堆或实时设置的大小而增加、支持从8MB 到16TB 的堆内存。ZGC 的一个核心设计原则是使用读屏障(load barrier)和着色指针(colored pointer)。在Java 中加载对象的行为会受到读屏障的影响,而着色指针具有供读屏障使用的信息,它使ZGC 能够查找、标记、定位和重新映射对象,这有助于降低垃圾回收的开销并极大降低停顿时间,且对吞吐量影响最大不超过15%[9]。作为一个并发的垃圾收集器,ZGC 所有的工作都是在Java 应用程序线程执行时完成的,这极大地减少了垃圾回收对应用程序响应时间的影响。而且Z 回收器现在还处于持续开发阶段,后续的开发目标是达到垃圾回收停顿时间不超过1ms。
SPEC jbb2015 测试模拟了一个典型的商业应用的三层架构环境中的中间层工作,包含商业逻辑、对象操作等,目的是衡量服务器Java 应用之性能。模型建立在一个全球型连锁超市的IT 基础架构之上,通过线上线下购物、库存管理、供应链管理、用户购买行为的数据挖掘等业务来评估整个系统的吞吐量及响应时间随着整个系统业务量不断增加时的性能表现[10]。SPECjbb2015 支持多种测试运行配置、支持虚拟化以及云环境,使用户能够全面分析和解决可能存在于包括硬件、操作系统、JVM 和应用程序层的性能瓶颈问题。
SPEC jbb2015 测试结果包含主要包含两个测试指标Max-jOPS和Critical-jOPS。最大性能指标Max-jOPS 是系统最大每秒钟处理的Java 操作数,可以看做在业务响应不失败的情况下,服务器的极限吞吐量,反映的是系统极限Java 应用性能。关键性能指标Critical-jOPS 是系统在5 个关键SLA(服务水平协议)10ms,25ms,50ms,75ms 和100ms 响应时间下平均每秒Java 操作数。 选择这些点是为了保证不同行业使用的响应时间目标的合理分布,可以看做衡量的是在响应时间有限的情况下的系统吞吐量。
测试环境选择使用四路服务器,配置4 颗Intel Xeon Platinum 8180 CPU,物理内存大小为1536G,操作系统为Red Hat Enterprise Linux 7.6,使用JDK 版本为Oracle Java SE 13.0.2,在此环境下分别使用-XX:+UseSerialGC(串行回收器)、-XX:+UseParallelOldGC(并行回收器)、-XX:+ UseConcMarkSweepGC(CMS 回收器)、-XX:+UseG1GC(G1 回 收 器)、-XX:+UseZGC(ZGC), 运 行SPEC jbb2015 测试,并使用参数-XX:+PrintGCDetails、-Xlog:gc.log 输出垃圾回收详细日志[11]。
图1 反映了几种垃圾回收器在SPEC jbb2015 基准测试中的性能表现,可以看出在极限性能指标Max-jOPS 上,并行回收器ParallelOldGC 表现最好,这是由于Max-jOPS 代表系统极限吞吐量而并行回收器在所有垃圾回收器中最侧重于吞吐量,因此这一指标明显优于其他垃圾回收器。而在关键性能指标上,ZGC 的表现最好,证明在一些有特定响应时间要求的业务场景下,其综合性能最好。ZGC 由于低停顿时间的特性,每次垃圾回收停顿时间不会超过10ms,这要远远低于其他垃圾回收方式(见图2)。一般来说,停顿时间的减少但带来的影响必然是吞吐量的降低,ZGC 垃圾回收处理工作都在与应用线程并发执行,同时也会不可避免地占用很多CPU 并发工作导致吞吐量降低。但如果同时考虑低响应时间与吞吐量情况下,ZGC 吞吐量降低的程度在可接受的范围之内,但同时其停顿时间减少了一个量级,因此其综合性能表现更好。
图2 展示了几种垃圾回收方式的平均停顿时间。
其中ZGC 的停顿时间最短,看到ZGC 虽然目标定位在停顿时间不超过10ms,但在实际测试时ZGC 平均停顿时间约为2ms,要远远低于10 毫秒的目标。串行回收器的停顿时间最长,几乎达到秒级,这是由于串行回收器回收时只有单线程,因此停顿的时间要远远高于其他垃圾回收器,因此在实际业务模型中性能很差。而并行回收器、G1 回收器平均停顿时间相差不大,都在100ms 上下,CMS 回收器则平均在200ms 左右。
本文简述了几种常用的JVM 垃圾回收方式及其原理,并利用业内权威的Java 应用性能测试工具SPEC jbb2015 测试了几种垃圾回收方式的实际性能。因为SPEC jbb2015 测试模型则具有广泛的代表性,所以对基于此项测试对垃圾回收的研究有助于开发者在实际应用中合理地对性能进行调优并解决性能问题。根据SPEC jbb2015 测试结果来看,当侧重于追求最大吞吐量,如基于Java的后台计算型应用、事务处理时,并行垃圾回收器ParallelOldGC表现更好。如追求低停顿时间,快速响应如互联网应用、web前端等时ZGC 优势明显,或者也可以使用G1 垃圾回收器配合-XX:MaxGCPauseMillis 参数来限制最大垃圾回收停顿时间。且ZGC 所支持的超大内存也非常适合需要大量内存的应用程序,比如大数据应用程序。但由于实际业务性能考量标准不同,还需结合每种垃圾回收方式自身特点,合理进行选择[12]。