目录
前面几篇文章我们讲了垃圾收集相关的算法,现在我们开始讲垃圾收集器。
有了虚拟机就一定需要手机垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector。
在了解垃圾收集器之前,我们要先了解评价垃圾收集器好坏的性能指标。
GC性能指标整体说明
评估GC性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例
- (总运行时间:程序的运行时间+内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
- 收集频率:相对于应用程序的执行,收集操作发生的频率
- 内存占用:Java堆区所占用的内存大小
- 反应速度:从一个对象变成垃圾到这个对象被回收的时间
不可能三角
吞吐量、暂停时间和内存占用这三者共同构成一个“不可能三角”,三者总体表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中两项。
这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占多多点也无关紧要,硬件性能的提升也有助于降低收集器运行时间对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。
简单来说主要抓住两点:
- 吞吐量
- 暂停时间
吞吐量与暂停时间的对比说明
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
- 比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。
吞吐量优先意味着在单位时间内,STW的时间最短:0.2+0.2=0.4
吞吐量VS暂停时间
高吞吐量较好,因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作,直觉上,吞吐量越高程序运行越流畅。
低暂停时间(低延迟)较好,因为从最终用户的角度来看,不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。因此,具有低暂停时间是非常重要的,特别是对于一个交互式应用程序。
不幸的是,“高吞吐量”和“低暂停时间”是一对相互竞争的目标(矛盾)。
- 因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。
- 相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的所见和导致程序吞吐量的下降。
在设计(或使用)GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或较小暂停时间),或尝试找到一个两者的折中。
现在的标准:在最大吞吐量优先的情况下,降低停顿时间。
垃圾收集器发展史
这是几款比较经典的垃圾收集器,我总结了一下它们的发展史。
出现的年份 | 出现在哪一版本 | 名称 | 备注 |
1999年 | JDK 1.3.1 | Serial GC |
ParNew GC 是 Serial GC 的多线程版本 |
2002年 | JDK1.4.2 | Parallel GC 和 CMS |
Parallel GC 在 JDK6 之后成为 Hotspot 默认GC。 |
2012年 | JDK1.7u4 | G1 |
|
2017年 | JDK9 | G1 |
G1 成为默认垃圾收集器,以替代 CMS |
2018年3月 | JDK10 | G1 |
G1 的并行完整垃圾回收,实现并行性能改善最坏情况的延迟。 |
2018年9月 | JDK11 | Epsilon GC |
同时引入 ZGC |
2019年3月 | JDK12 发布 | 增强 G1 |
增加 G1 ,自动返回未使用堆内存给操作系统; 同时,引入Shenandoah GC |
2019年9月 | JDK13 发布 | 增强 ZGC |
增强 ZGC ,自动返回未使用堆内存给操作系统 |
2020年3月 | JDK14 | 删除 CMS |
扩展 ZGC 在 mac 和 windows 的应用 |
垃圾收集器分类
按碎片处理方式
- 压缩式垃圾回收器:压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
- 再分配对象空间使用:指针碰撞
- 非压缩式垃圾回收器不进行这步操作。
- 再分配对象空间使用:空闲列表
按工作的内存区间分
- 可分为年轻代垃圾回收器和老年代垃圾回收器
不同垃圾收集器详解
Serial
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
Serial收集器的工作过程如下图:
迄今为止,Serial收集器依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中继续讲解。Serial Old收集器的工作过程如图:
没错,和Serial的工作工程一样。
ParNew
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
ParNew收集器的工作过程如下图:
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
cms的简单介绍
在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
可以说直到CMS的出现才巩固了ParNew的地位,但成也萧何败也萧何,随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。所以自JDK 9开始,ParNew加CMS收集器的组