「 JVM 」常见的垃圾收集器Garbage collector(GC)
参考&鸣谢
【JVM系统学习之路】常见垃圾回收器 山间木匠
Java 的七种垃圾收集器 | Linux 中国 Jayashree Huttanagoudar
带你走近Java虚拟机到底有哪些经典的垃圾收集器 码上遇见你
一、GC 分类
垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的 JVM 来实现。由于 JDK 的版本处于高速迭代过程中,因此 Java 发展至今已经衍生了众多的 GC 版本。从不同角度分析垃圾收集器,可以将 GC 分为不同的类型。
Java不同版本新特性
- 语法层面:Lambda表达式、switch、自动拆箱装箱、enum
- API层面:Stream API、新的日期时间、Optional、String、集合框架
- 底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化
垃圾收集器分类
按线程数分
垃圾回收线程数,可以分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
- 在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的 Client 模式下的 JVM 中
- 在并发能力比较强的 CPU 上,并行回收器产生的停顿时间要短于串行回收器。
和串行回收相反,并行收集可以运用多个 CPU 同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制。
按工作模式分
按照工作模式分,可以分为 并发式垃圾回收器
和 独占式垃圾回收器
。
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
- 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
按碎片处理方式分
按碎片处理方式分,可分为 压缩武垃圾回收器
和 非压缩式垃圾回收器
。
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
- 非压缩式的垃圾回收器不进行这步操作。
按工作的内存区间分
又可分为 年轻代垃圾回收器
和 老年代垃圾回收器
。
二、评估GC的性能指标
-
吞吐量:运行用户代码的时间占总运行时间的比例
-
- (总运行时间 = 程序的运行时间 + 内存回收的时间)
-
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
-
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-
收集频率:相对于应用程序的执行,收集操作发生的频率。
-
内存占用:Java堆区所占的内存大小。
-
快速:一个对象从诞生到被回收所经历的时间。
吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。简单来说,主要抓住两点:
- 吞吐量
- 暂停时间
**性能指标:吞吐量(throughput)**吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的吞吐量优先,意味着在单位时间内,STW 的时间最短:0.2+0.2=0.4性能指标:暂停时间(pause time)“暂停时间”是指一个时间段内应用程序线程暂停,让Gc线程执行的状态例如,GC期间1ee毫秒的暂停时间意味着在这1e0毫秒期间内没有应用程序线程是活动的。暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5
吞吐量vs暂停时间高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。低暂停时间(低延迟)较好因为从最终用户的角度来看不管是 GC 还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的 200 毫秒暂停都可能打断终端用户体验。因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。在设计(或使用)GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。现在标准:在最大吞吐量优先的情况下,降低停顿时间
三、不同的垃圾回收器概述
垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率。这当然也是面试的热点。那么,Java 常见的垃圾收集器有哪些?
GC 垃圾收集器是和 JVM 一脉相承的,它是和 JVM 进行搭配使用,在不同的使用场景对应的收集器也是有区别
垃圾回收器发展史有了虚拟机,就一定需要收集垃圾的机制,这就是 Garbage Collection,对应的产品我们称为 Garbage Collector。
- 1999年随JDK 1.3.1 一起来的是串行方式的 SerialGc,它是第一款 GC。ParNew 垃圾收集器是 Serial 收集器的多线程版本
- 2002年2月26日,Parallel GC 和 Concurrent Mark Sweep GC 跟随 JDK1.4.2 一起发布·
- Parallel GC 在 JDK6 之后成为 HotSpot 默认 GC。
- 2012年,在 JDK1.7u4 版本中,G1 可用。
- 2017年,JDK9 中 G1 变成默认的垃圾收集器,以替代 CMS。
- 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
- 2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
- 2019年3月,JDK12 发布。增强 G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。·2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
- 2020年3月,JDK14 发布。删除 CMS 垃圾回收器。扩展 ZGC 在 macos 和 Windows 上的应用
7种经典的垃圾收集器
- 串行回收器:Serial、Serial old
- 并行回收器:ParNew、Parallel Scavenge、Parallel old
- 并发回收器:CMS、G11
7款经典收集器与垃圾分代之间的关系
- 新生代收集器:Serial、ParNew、Paralle1 Scavenge;
- 老年代收集器:Serial old、Parallel old、CMS;
- 整堆收集器:G1;
垃圾收集器的组合关系
- 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial old、Serial/CMS、ParNew/Serial old、ParNew/CMS、Parallel Scavenge/Serial 0ld、Parallel Scavenge/Parallel 0ld、G1;
- 其中Serial old 作为 CMS 出现"Concurrent Mode Failure"失败的后备预案。
- (红色虚线)由于维护和兼容性测试的成本,在 JDK 8 时将 Serial+CMS、ParNew+Serial old 这两个组合声明为废弃(JEP173),并在 JDK9 中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK14中:弃用 Parallel Scavenge 和 Serialold GC 组合(JEP366)
- (青色虚线)JDK14中:删除 CMS 垃圾回收器(JEP363)
为什么要有很多收集器,一个不够吗?因为Java的使用场景很多,移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。Serial垃圾收集器
Serial是一个新生代收集器,使用的是标记复制算法,Serial收集器是最基础也是历史最悠久的收集器,Serial是属于单线程垃圾回收器,在进行垃圾回收时会暂停所有的用户线程,直到垃圾回收结
如何查看默认垃圾收集器-XX:+PrintcommandLineFlags
:查看命令行相关参数(包含使用的垃圾收集器) 使用命令行指令:jinfo -flag
相关垃圾回收器参数 进程ID
Serial垃圾收集器
Serial是一个新生代收集器,使用的是标记复制算法,Serial收集器是最基础也是历史最悠久的收集器,Serial是属于单线程垃圾回收器,在进行垃圾回收时会暂停所有的用户线程,直到垃圾回收结束(Stop The World)。
上图所示为Serial收集器工作示意图,Serial在JDK1.3之前是新生代垃圾回收的唯一选择,虽然Serial出现时间很久并且简单,但是一直都是HotSpot虚拟机运行在Client模式下的默认新生代收集器。
TIPS:JVM有两种运行模式Server与Client。两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。
32位默认为Client模式,64位只支持Server模式。
优势:相比于其他垃圾收集器的单线程,Serial更加简单高效,在单个CPU环境下,Serial收集器不需要进行额外的线程切换,有更高的效率。
设置参数: -XX:+UseSerialGC:指定使用Serial垃圾收集器。
ParNew垃圾收集器
ParNew是新生代收集器,使用的算法和Serial一样都是标记复制算法,ParNew收集器就是Serial收集器的多线程版本, 两者的区别就是Serial使用单线程进行垃圾回收,ParNew使用多条线程进行垃圾回收,两者使用的垃圾回收算法、Stop The World和回收策略等其他方面都完全一样。
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
ParNew收集器在单CPU环境下的垃圾收集效率不会比Serial更好,因为ParNew存在线程交互的开销,然而,随着可以使用的CPU的数量的增加,ParNew收集器对资源的有效利用越来越好。
在单CPU的环境下,由于只有一个CPU核心在工作,所以ParNew收集器和Serial收集器的工作原理都是基于单线程模式的。这时,它们的垃圾收集效率主要受限于单线程的处理能力。
ParNew收集器虽然可以使用多线程进行垃圾回收,但是在单CPU环境下,多线程的并行处理只会引入额外的线程切换开销,因为同一时间只有一个线程能够占用CPU资源。这反而会降低垃圾收集的效率。
与之相比,Serial收集器虽然也只是单线程模式,但是由于它不需要考虑多线程的并行处理,因此在单CPU环境下,它能够更加高效地利用CPU资源,避免了无谓的线程切换开销,因此性能会比ParNew更好。
需要注意的是,上述结论是建立在单CPU环境下的情况下。在多CPU环境下,ParNew收集器会更加适合,因为可以充分利用多个CPU核心的并行处理能力。
Parallel Scavenge垃圾收集器
Parallel Scavenge是一个新生代收集器,使用的算法是标记复制算法,和ParNew一样属于并行垃圾收集器,其运行示意图可看ParNew收集器示意图。
Parallel Scavenge的特点在于它的关注点和其他收集器不同,在之前介绍的收集器以及后面要介绍的CMS收集器中,更加关注的是如何减少用户线程的停顿时间(Stop The World),而Parallel Scavenge关注的是吞吐量(Throughput)。
吞吐量:运行用户代码时间和CPU总消耗时间比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
例如一个CPU运行用户代码时间是90分钟,垃圾收集时间是10分钟,那么吞吐量 = 90 / (90 + 10) = 90%
停顿时间短的垃圾收集器适合与用户交互频繁的程序,较少的停顿能够提高用户体验,而吞吐量大的垃圾收集器可以高效率利用CPU,主要适用于后台运算而不需要太多交互的内容。
Serial Old垃圾收集器
Serial Old收集器就是Serial收集器的老年代版本,同样是一个单线程的收集器,使用的是标记整理算法,这个收集器是虚拟机在Client模式下的老年代默认收集器,如果在JDK1.5之前版本的Server模式下,Serial Old收集器和Parallel Scavenge收集器搭配使用,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器作为老年代收集器,但是PS MarkSweep收集器与Serial Old实现非常接近,所以直接以Serial Old代替PS MarkSweep进行讲解。在JDK1.5版本之后Serial Old收集器则作为CMS收集器的后备预案。
Parallel Old垃圾收集器
Parallel Old收集器收集器是Parallel Scavenge收集器的老年代版本,使用的是标记整理算法,在JDK1.6中开始提供,在JDK1.6之前,如果新生代收集器选择了Parallel Scavenge,老年代就只能选择Serial Old(PS MarkSweep)收集器,由于老年代Serial Old收集器使用单线程在服务端应用性能上的拖累,即便使用了Parallel Scavenge收集器,它的吞吐量效果也无法展现出来。
直到Parallel Old收集器出现,它的关注点同样在吞吐量,因此Parallel Scavenge和Parallel Old搭配使用,能最大程度的提升程序的吞吐量,在注重吞吐量和CPU资源敏感并且与用户交互少的场景下,可优先使用该组合。
CMS垃圾收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,采用的是标记清除算法。
CMS收集器在进行垃圾收集时,一共有四个步骤:
- 初始标记(CMS initial mark):单线程执行,标记出GC Roots能直接关联到的对象,速度较快,会发生Stop The World。
- 并发标记(CMS concurrent mark):并发执行,用户线程正常运行,垃圾收集线程进行并发标记,也就是GC Roots Tracing,对初始标记的对象进行追踪标记,该阶段时间较长,但是不会发生Stop The World。
- 重新标记(CMS remark):由于并发标记阶段用户线程仍在执行,所以可能会产生垃圾,重新标记阶段就是将并发标记阶段发生变动了的对象标记进行修正,该阶段会发生Stop The World,停顿时间比初始标记阶段稍长,比并发标记阶段短。
- 并发清除(CMS concurrent sweep):并发执行,该阶段直接清楚之前标记的垃圾,用户线程可继续执行,不会产生Stop The World。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起 工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
G1垃圾收集器
G1(Garbage – First)垃圾收集器是面向服务端的垃圾收集器,使用G1垃圾收集器时,Java堆空间中不再和之前一样划分为Eden、Survivor和Old区,而是将Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以是Eden、Survivor或者Old,还有一种特殊的区域,叫Humongous区域,如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个大对象,就会分配到Humongous区域。
上图所示的就是通过Region划分的Java堆区,在该划分方法中,Eden、Survivor和Old不再是区分开的,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
**并行与并发:**G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(或CPU核心)来缩短Stop-The-World停顿的时间,其他收集器原本需要停顿用户线程执行的GC动作,G1收集器可以通过并发的方式让用户线程和GC线程同时执行。
**空间整合:**从整体来看,G1采用的是标记整理算法,从局部(两个Region间)看,是基于复制算法,所以G1收集器不会产生内存碎片。
**可预测的停顿:**低停顿的同时实现高吞吐量,建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这样就保证了在有限的时间内尽可能提高效率。
G1垃圾收集器工作时可以分为4个步骤,其中初始标记、并发标记以及最终标记和CMS收集器的前三个步骤有很多相似之处。
- 初始标记(Initial Marking)
标记出GC Roots能直接关联到的对象,修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象速度较快,会发生Stop The World。
- 并发标记(Concurrent Marking)
从GC Root开始对堆中对象进行可达性分析,找出存活的对象,与用户线程并发执行,耗时较长。
- 最终标记(Final Marking)
将并发标记阶段发生变动了的对象标记进行修正,将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,并行执行,会发生Stop The World。
- 筛选回收(Live Data Counting and Evacuation)
- 首先对各个Region的回收价值和成本进行排序。
- 然后根据用户所期望的GC停顿时间来制定回收计划。
- 最后按计划回收一些价值高的Region中垃圾对象。
- 回收时采用”复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存。
- 该阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
四、总结
JVM垃圾回收器是Java开发中非常重要的一部分,它们对应用程序性能和稳定性有着至关重要的作用。在本文中,我们学习了垃圾回收器的分类、评估垃圾回收器的性能指标以及不同垃圾回收器的概述。希望这篇文章对您有所帮助,并且可以更好地理解和应用垃圾回收器相关的知识。