裴佳豪:垃圾回收器的前世今生
裴佳豪,来自中原银行信息技术部技术平台组,目前从事后端开发工作。
之前在工作中遇到了服务半夜下线,通过grafana监控数据分析,推断是发生了Full GC,导致应用线程中断时间过长,以至于注册中心对应用的心跳检测失败,将该服务从注册中心剔除掉。
那什么是Full GC?首先要说明GC的含义:Garbage Collection,垃圾回收。Full GC就是对内存中大部分区域进行垃圾回收,此时用于垃圾回收的线程会独占CPU,因此应用程序会被挂起,无法运行。
那就有三个问题:垃圾是谁?从哪来?到哪去?
先上两行代码。代码运行所在的内存相当于一个屋子,a相当于一个柜子,这个柜子本来装着衣服,后来把衣服换成了鞋子,但此时衣服还在屋子里,之后再没穿过,它就是垃圾了,代码运行到括号外面了,那这个柜子也成垃圾了,因为用不到了。把这些垃圾清理了,拿回空间就能放其他用的上的东西了。
图1 例子说明
垃圾就是程序使用后,不再使用、又无法被再次利用的空间;垃圾由应用程序产生,被清理后对应空间可再被利用。
最初还没有GC,程序员需要自己手动进行内存管理,申请内存,存放对象并没太大问题,但释放所有不再使用的内存空间,难度就会很大,如果忘记释放,会发生内存泄露,内存堆满后可能会导致系统崩溃;而如果错误释放了正在使用的空间,程序就会出现bug。
因此技术大佬们就研究设计了GC,让计算机来自动管理内存,程序员只需专注在业务编码上。但过度依赖于“自动”,就会弱化开发人员在程序出现内存溢出时定位问题和解决问题的能力。因此了解垃圾回收原理能够提升解决内存相关问题的能力,以及性能调优的能力。
本节简单介绍下垃圾回收算法的实现原理,作为后文介绍垃圾回收器的铺垫。
1960年出现第一个GC算法:标记—清除算法。这个算法首先会标记出所有需要回收的对象,标记完成后统一回收,缺点是会产生内存碎片。
图2 标记清除算法
1963年出现复制算法。此算法将可用内存分为两块,每次只使用一块。当这块的内存用完,将还存活的对象复制到另一块,然后清理使用过的内存空间。复制算法不会有内存碎片的问题,但该算法的缺点是只有一半内存可用,内存浪费严重,而且如果垃圾较少,需复制的对象较多,则耗时较长。
图3 复制算法
在这之后研究的许多算法可以说都是基于上述两个算法,进行组合或应用而实现的。例如标记—整理算法:组合标记—清除算法与复制算法,标记出所有需要回收的对象,让所有存活的对象都聚到一端,然后清理端边界以外的内存。解决内存碎片以及内存浪费的问题,但缺点是算法复杂,需考虑对象引用问题。
图4 标记整理算法
每种算法都有其优劣点,最好的方法是适合的场景用适合的算法。因此提出了分代垃圾回收这个策略,导入了年龄的概念,将对象分类成新生代与老年代,针对不同代使用不同的GC算法。新生代中存放刚生成的对象,这些对象大部分生命周期较短,适合复制算法,需复制的对象不多,速度快并且无内存碎片问题。在经过一定次数的新生代GC的对象被当作老年代对象,这种情况称作晋升(promotion)。老年代对象生命周期长,不适合复制算法,更适合标记-清除和标记-整理算法。
图5 分代垃圾回收
分代回收仅考虑了对象的生命周期,无法控制GC回收的大小。因此分区回收算法应时而生,将整个堆划分成连续的小区间,每个小区间独立使用和回收,GC时可以控制要回收区间的数量,从而控制GC产生的停顿时间,分区间采用复制算法,效率更高。
图6 分区垃圾回收
评价指标
有了垃圾回收算法,对应的程序实现就是垃圾回收器。但即便基于相同的算法原理,回收器也会有不一样的实现,呈现出来的性能效果也各有倾向。
而考量一个垃圾回收器是否适用主要有两个指标:吞吐量、最大暂停时间。
·吞吐量
负责GC的线程会和应用程序线程争用当前可用CPU的时钟周期。应用程序线程用时占程序总用时的比例即为吞吐量。吞吐量越高越好。
·最大暂停时间
GC时会出现应用程序线程挂起,仅GC线程运行的场景。出现此现象最长的一次的时间就是最大暂停时间。暂停时间越短越好。
但吞吐量和最大暂停时间是相互矛盾的,吞吐量大意味着GC次数少,而GC少则意味着一次GC会有更多的垃圾需要处理,暂停时间就会变长。因此,若是交互式的应用,注重用户体验,则选择暂停时间更短的垃圾回收器;若应用追求短时间完成任务,则选择吞吐量更高的回收器。
今生发展
回收器的发展可以分成三个阶段:串行、并行、并发。同时从算法层面又主要分为分代收集与分区收集。
串行回收器简单高效,无线程切换的开销,适合单核环境、内存较小的场景。主要实现由Serial和Serial Old回收器,采用分代收集。
Serial:jdk1.3之前新生代回收的唯一选择,使用复制算法,单线程独占CPU进行回收。
Serial Old:老年代版的Serial,使用标记-整理算法,也是CMS收集器的后备收集器。
但如今计算机内存至少G级别,并行计算能力也更强,因此在串行回收器上进一步改进,实现并行回收器,仍然独占CPU,但多线程处理速度更快,暂停时间更短。主要实现有ParNew、Parallel Scavenge和Parallel Old,同样采用分代收集。
ParNew:用于新生代收集,相当于多线程版本的Serial回收器,使用复制算法,常与 CMS 组合使用。
Parallel Scavenge:jdk1.4正式发布,jdk1.6时作为默认新生代回收器。使用复制算法,作为新生代回收器。不同在于ParNew追求低停顿时间,Parallel Scavenge追求高吞吐量,短时间完成任务,但不适合交互式应用。
Parallel Old:jdk1.6正式使用,jdk1.7作为默认老年代回收器。Parallel Scavenge的老年代版本,使用标记-整理算法,追求高吞吐量。
并行回收器虽然采用多线程的方法缩短了暂停时间,但整个GC过程仍然是独占CPU的,进一步改进,采用多线程并发回收。并发回收器的回收流程中仅小部分环节是独占CPU,会产生暂停时间,其余环节则与应用线程并发执行,进一步缩短暂停时间。CMS与G1是当前应用最广泛的并发回收器。
CMS:全称:Concurrent Mark Sweep。jdk1.4正式发布,作为老年代回收器。使用标记—清除算法,并使用分代收集,默认ParNew收集器作为新生代收集器。CMS是里程碑式的回收器,开启了并发回收的时代,但它从未被作为默认回收器,并且在JDK9被标记弃用,JDK14被删除。原因就在于CMS有许多问题,包括吞吐量低、产生浮动垃圾、产生内存碎片。尤其G1回收器的发布,便替代了CMS。
G1:全称:Garbage First。jdk1.7正式发布,jdk9作为默认收集器,工作在新生代以及老年代。G1使用分区算法。G1基于分区,能够更细粒度的回收,并且在区域间基于复制算法来回收,从而实现压缩。因此相比CMS内存碎片少很多。可以指定预期停顿时间,收集器会根据预期目标,只回收价值大(垃圾多)的区域,而CMS需要扫描整个老年代进行回收。
图7 收集器
回到文章开头提到的服务下线的问题,可以考虑G1来替换CMS来提升性能,但口说无凭,实践对比。
首先是JVM参数配置,根据服务器的内存来分配,测试机内存8G,考虑其他应用占用,JVM配置4G。
图8 参数配置
从配置参数上就能看出明显差异,CMS需要配置多达9个参数,G1因为具有自适应的特性,则只需配置一个参数,极大减轻开发人员学习成本,将更多精力放在功能开发上。
基于同一段代码,测试CMS与G1,代码中会不断生成小对象,并间断的生成4M大对象。CMS与G1的表现如下图所示。
图9 使用CMS垃圾回收器
图10 使用G1垃圾回收器
从CPU占用率以及停顿时间方面对比,G1表现都要更优秀。后续又对CMS进行简单调优后,较好的表现是新生代收集45次,耗时7.74秒,老年代收集8次,耗时0.74秒。相比之前性能好了许多,但相比G1仍然要差。
因此,如果是新系统或者系统使用CMS后出现过内存回收引起的溢出等问题,推荐使用G1回收器,但G1更适合大内存的服务使用。如果服务器内存较小,应用内存吃紧,始终需进行整个堆的回收,G1的工作量并不少,甚至因为算法更复杂,性能更差,不推荐改用G1。
前文提到垃圾回收器的评价指标主要为吞吐量与最大暂停时间。未来垃圾回收器的发展趋势主要是缩短暂停时间,这其实也与当前软件更注重用户体验的趋势一致。
ZGC:jdk11发布。基于分区,采用复制算法,并进行改进,改进后的算法基本全程并发运行。今年3月份正式发布的jdk16里对ZGC进一步优化。暂停时间达到O(1) 级,并且不随着内存大小而增大。即使是TB级的内存,平均暂停时间也约为0.05毫秒,最大暂停时间则约为0.5毫秒。更适合大内存、低延迟的服务进行内存回收。
shenandoah:jdk12发布的。基于分区,采用复制算法,并进行改进。暂停时间比ZGC要长,在10毫秒左右,但吞吐量比ZGC更高,暂停时间同样与内存大小无关。
还有其他的一些收集器,比如Azul 的 Zing JVM使用的 C4(Concurrent Continuously Compacting Collector)收集器 、用于分布式垃圾回收的DGC。篇幅原因,不再一一介绍。
垃圾回收的内容有非常多,本文只是引子。各位同事有兴趣的话,可以继续深耕。
1.深入Java虚拟机:JVM G1GC的算法与实现 中村成洋
2.《垃圾回收的算法与实现》中村成洋/相川光
3.《深入理解Java虚拟机》 周志明
4.Java中9种常见的CMS GC问题分析与解决 美团
5.Garbage-First Garbage Collection David Detlefs / Christine Flood
6.Major GC和Full GC的区别 RednaxelaFX
7.G1: One Garbage Collector To Rule Them All Monica Beckwith
8.ZGC | What's new in JDK 16 perliden
9.OpenJDK16
深入Java虚拟机:JVM G1GC的算法与实现 中村成洋
《垃圾回收的算法与实现》 中村成洋 / 相川光
《深入理解Java虚拟机》 周志明
Java中9种常见的CMS GC问题分析与解决 美团
Garbage-First Garbage Collection David Detlefs / Christine Flood
Major GC和Full GC的区别 RednaxelaFX
G1: One Garbage Collector To Rule Them All Monica Beckwith
ZGC | What's new in JDK 16 perliden
OpenJDK16
图文 | 裴佳豪
排版 | 张艺冉