背景知识
指标与权衡
吞吐量和暂停时间(垃圾收集时间,STW)是最重要的两个指标。从吞吐量公式来看,垃圾收集时间和吞吐量是互相权衡的。
吞
吐
量
=
运
行
用
户
代
码
时
间
(
运
行
用
户
代
码
时
间
+
垃
圾
收
集
时
间
)
吞吐量 = \frac{运行用户代码时间}{(运行用户代码时间+垃圾收集时间)}
吞吐量=(运行用户代码时间+垃圾收集时间)运行用户代码时间
对于交互式要求高的应用来说,例如客户端,一般要求高响应,即每次尽可能短的垃圾收集时间。
对于高性能需求的应用来说,例如服务端,一般强调高吞吐里。
垃圾收集器发展历史
了解一下技术发展历史还是比较有用的。
- 1999年随JDK1.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上的应用
经典垃圾收集器分类
以下是两种比较重要的分类,以及垃圾收集器直接的配合关系,这个组合关系比较重要。
并行串行并发
- 串行回收器:Serial、Serial Old
- 并行回收器:ParNew、Parallel Scavenge、Parallel old
- 并发回收器:CMS、G1
新生代老年代
- 新生代收集器:Serial、ParNew、Parallel Scavenge;
- 老年代收集器:Serial Old、Parallel Old、CMS;
- 整堆收集器:G1;
组合
- 红线部分表示在JDK9之后弃用(JDK8时已经申明过期)
- 绿线表示JDK14中弃用,而且弃用的包括CMS GC本身。
- 从图中来看,事实上ParallelScavengeGC在JDK8之后就完全替换了ParNewGC,使得后者在后期基本不使用。
通用参数命令
需要知道经典收集器的概述以及适用场景。
-XX:+PrintCommandLineFlags
:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
Serial收集器:串行
概述
最大的特点就是串行。
参数设置
使用-XX:+UseSerialGC
参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC。
回收算法
简单,新生代版本采用复制算法,老年代版本采用标记-压缩算法。
优点分析
简单高效,没有线程切换开销,在单核单线程的场景下回收效率比较高,可以想象,在回收频率不大或小型应用的情况下STW可以接受,吞吐量应该还不错。
适用
目前是client模式下默认的老年代垃圾收集器,在早期也经常作为老年代的垃圾收集器配合使用,因为老年代回收频率比较低。现在一般作为低延迟收集器的兜底方案。
ParNew收集器:最原始的多线程回收
概述
最大的特点就是和Serial非常像,只是新生代回收变成并行的,强调高吞吐量。
回收算法
只用于新生代,采用复制算法。、
优点分析
并行的重点就是强调多核情况下高吞吐量。
适用
基本不用。
在早期,它经常作为新生代的垃圾收集器,和SerialOld配合使用。但它只是在新生代收集的时候并发,所以后来被Parallel完全替换,目前基本没有使用
参数设置
-XX:+UseParNewGC
"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
Parallel收集器:自适应吞吐量
概述
最大的特点是,ParNew有的它都有,它也是并发收集,从图中来看,它改进是,老年代同样也启用并发收集。另一方面,它同样强调高吞吐量,但是它是可控的高吞吐量,它可以更具一些参数自适应地调节堆的大小。
回收算法
新生代版本使用复制算法,老年代版本使用标记-压缩算法。
优点分析
强调并发高吞吐量,而且在不知怎么调节的情况下,自适应策略是一种比较合适的配置。从并发高吞吐量的倾斜来看,它的交互性会比较差。
适用
当前是JDK8默认的垃圾收集器,不管是自适应性还是并发高吞吐量,完全替代了ParNew。适用于后台或服务器这种强调性能,轻交互性的场景:执行批量处理,订单处理,计算任务等。
参数设置
-
-XX:+UseParallelGC
手动指定年轻代使用Parallel并行收集器执行内存回收任务。-XX:+UseParallelOldGC
手动指定老年代都是使用并行回收收集器。 二者相互激活。 -XX:ParallelGCThreads
设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间。-XX:GCTimeRatio
垃圾收集时间占总时间的比例。-XX:+UseAdaptivesizePolicy
设置Parallel Scavenge收集器具有自适应调节策略。这三个参数,设置之后会通过动态调节堆的大小,来满足参数的要求,这也是所谓的自适应。
CMS收集器:第一个真正并发
整体描述
概述
老年代的垃圾收集,它是第一个正在意义上的并发收集器——用户线程和垃圾回收线程并发执行。
上述的所有垃圾收集器其实都需要有一段很长的STW时间,所以优化的话第一考虑就是对STW进行优化——通过拆分的方式,找出独占部分(垃圾收集线程)和可以并发部分(垃圾收集线程和用户线程可以并发)。
回收算法
只用于老年代,采用标记-清除算法。很好理解,如果采用整理的话,清理阶段存活对象引用会发生变动,这个时候用户程序还在运行,那是不能的。
优点分析
真正的并发收集,强调低延迟。
缺点分析
由于CMS的机制带来的很多缺点,比较重要
- 浮动垃圾问题,并且如果晋升的时候没有空间,会触发串行兜底方案,SerialOld。
- 内存碎片,需要定期在Full GC时整理
- CMS对CPU资源非常敏感,因为并发标记占用一部分线程,会降低整体吞吐量。
适用
被G1替代。
CMS GC适用于性能比较高同时要求响应比较快的场景,例如一些大互联网网站的后台。但是,作为老年代收集器,很遗憾无法和表现很好的ParallelGC一起使用,因而它只能选择Serial GC或者ParNewGC。所以它使用并不广泛。JDK10之后默认收集器为G1,而在JDK14之后CMS被取消。
参数设置
-XX:+UseConcMarkSweepGC
手动指定使用CMS收集器执行内存回收任务。
开启该参数后会自动将-xx:+UseParNewGC
打开。-XX:CMSInitiatingOccupanyFraction
设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。 CMS的特点在于垃圾收集的时候用户程序并发执行,这就可能产生新对象导致老年代放不下,所以必须提前进行垃圾回收。顺便一提,发生这种情况需要启用备用方案,启用SerialOld进行回收,它会进行标记-整理算法。-XX:+UseCMSCompactAtFullCollection
用于指定在执行完Full GC后对内存空间进行压缩整理,-XX:CMSFullGCsBeforeCompaction
设置在执行多少次Full GC后对内存空间进行压缩整理。这两个是为了及时清理内存碎片问题。-XX:ParallelcMSThreads
设置CMS的线程数量。
回收过程
CMS(Concurrent-Mark-Sweep)强调低延迟,高响应。具体而言,多次标记过程如下:
- 初始标记,STW,标记GCRoots,这个STW非常快
- 并发标记,并发引用分析,这里是全堆的全图扫描
- 重新标记,STW,并发标记阶段,这里进行的是增量更新,请参考《垃圾回收相关技术-并发标记和重新标记的原理》。
- 并发清理,采用标记清理。
G1收集器
整体描述
概述
从G1开始,垃圾收集器的设计开始倾向让回收效率高于内存分配效率,而不追求整堆的完整回收。这是很自然的,部分回收可以满足需求,而且可以直接减少延时时间。
G1收集器的最大特点是分区,建立“停顿时间模型”,它仍然需要较长的STW,但是它的最大特点是能在停顿时间可指定的情况下追求最大化的吞吐量。
回收算法
由于它回收的过程会把区域内保存的对象复制到其他区域,所以,区域之间,它是复制算法。
但是从整体回收过程来看,它又是基于标记-整理算法。
重要的是二者都不会产生内存碎片。
优点分析
G1收集器的最大特点是分区,建立“停顿时间模型”,它仍然需要较长的STW,但是它的最大特点是能在停顿时间可指定的情况下追求最大化的吞吐量。
相较于CMS,它支持部分回收。
缺点分析
- 内存占用高,每个区域都会有自己的记忆集,所以内存开销比较大
- 额外执行负载较高,因为记忆集需要大量的写屏障维护所以它的负载更大。
适用
在JDK 9之后,G1成为了默认的垃圾回收器,替代了CMS,直到现在。它在可预期的停顿时间下可以达到尽可能高的吞吐量,并且不会产生内存碎片,而且随着Hotspot的优化,它在不断的改进。
实现技术
分区
G1把内存划分为多个相同的Region,每个区域都可以扮演不同角色。
并且预留了Humongous区域,用于存放大对象。
混合回收
MixedGC模式,它打破了分代回收的樊笼,可以面向堆内存中的任何区域组成回收集,回收价值更大的部分回收区域,做到混合部分回收,衡量标准是回收的价值收益。
跨区域引用
部分回收带来的跨Region的引用,而且由于Region数量远比分代多,所以记忆集空间开销也要更大。
并发标记
并发标记阶段统一个Region如何做到同时收集,同时分配。并发标记问题上,G1采用的是更快的原始快照方式,每个Region划出专门的空间用于并发标记标记阶段的新对象分配。
可靠的停顿预测模型
如何建立可靠的停顿预测模型?每个Region会统计很多信息:平均花费成本,标准偏差,置信度等,这些统计信息使用“衰减的平均值”——强调最近状态的影响,用于预测停顿时间。
Mixed GC回收过程
- 初始标记,STW,收集GC Roots,G1在Minor GC时同步完成,并不占额外开销。
- 并发标记,扫描全堆对象图,并结合SATB(原始快照)记录引用改动。
- 最终标记,STW,扫描上面记录的引用改动,重新标记。
- 筛选回收,STW,选择价值高的Region,构成回收集,复制,回收。
从上面来看,STW的阶段还是大多数,但耗时的可达性分析可以并发完成,但它相对于CMS的一大优势是,筛选回收阶段只处理部分Region,包括复制、回收工作,这也是停顿时间可控的原因。
调优建议
正因为如此,鉴于停顿时间和回收区域数量相关,回收停顿时间默认设置为200ms,这个值如果指定太低,那么可想而知,每次只有少量区域被回收,当分配效率大于回收效率时,就会触发Full GC影响性能。
低延迟垃圾收集器
内存占用,吞吐量,延迟,三者是垃圾收集器的不可能三角——一款优秀的收集器通常同时达到两项。
其中最重要的延迟——随着硬件的发展,内存事实上是最不重要的,而我们可以想象,硬件性能必然会带动吞吐量的上升,只有延迟,内存越大,回收的对象越多,延迟就越大。所以延迟,成了优化的关键。这里介绍Shenandoah和ZGC两个收集器的一些技术,详细可以查看《深入理解Java虚拟机》。
Shenandoah收集器
它是G1的改进,主要有以下方面。
并发回收与转发指针
G1中回收阶段是STW的,shenandoah在这里进行优化。它的核心问题在于,并发回收阶段,如果对象移动,如何去维护其他引用该对象的指针。
这里它使用了转发指针的方式。在每个对象头部增加Brooks Pointer,该指针默认指向自己,发生移动的时候它会指向新的对象,如下图,从而解决运行中对象移动引用更新问题,它很像句柄定位。
这种设计很合理,但是很显然会遇到多线程竞争问题,需用通过CAS的方式来同步该转发更新过程。
其次,读写对象的频率实际上非常非常高,如果在每次读屏障的时候,它都需要增加额外的转发处理,那么开销会非常大,因而shenandoah使用额外的设置,只在更新引用的时候使用读屏障。
取消分代
只剩下区域的概念。
连接矩阵替代记忆集
连接矩阵很像图中的邻接矩阵,它记录了不同区域之间的跨区域引用关系。
ZGC收集器
ZGC是一款基于Region内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
—《深入理解Java虚拟机》
动态的Region
分区不再是固定大小,而是分为小型,中型,大型区域,而且大型区域的大小是动态的。
全堆扫描 - 无记忆集
由于ZGC标记、回收全部过程都可以并发执行,所以它实现全堆的扫描,并不进行部分回收(更像实时回收),自然也就没有部分回收引起的跨区域引用问题。正因为如此,它不需要再像G1一样,耗费大量空间时间维护记忆集。
染色指针与内存多重映射
染色指针是ZGC实现并发回收,整理的关键。并发的回收的关键在于,你必须在移动对象之后,通过额外的机制来更新对象上的引用,例如shenandoah就使用了转发指针,但事实上开销并不小。
如果对象被移动,能不能在不访问对象的情况下,直接从指针或是其他地方获得对象的一些相关信息呢?例如三色标记,只跟引用有关,和访问到对象无关。
染色指针是把这些信息放在指针的高位——当前系统中,并没有完全使用64位的索引空间。这样一来,就能从指针上直接判断该对象的三色状态,是否被移动,如果被移动,则直接使用维护的转发表,找到新的对象位置,并更新,这也叫染色指针的自愈——即时更新移动地址。
这样一来,染色指针极大地减少了读屏障的数量(对比shenandoah),提高了性能。
- 这里边还有一些问题我不太理解:例如一个对象肯定被多处引用,如何同时维护多个指针的染色信息?
内存多重映射
JVM作为一个普通的进程,随意定义内存中某些指针,操作系统能否支持?答案是否定的,系统只会将其作为一个普通地址,所以,为了解决这个问题,ZGC引入了内存多重映射,把更大空间的虚拟地址(带染色信息)映射到真实的地址上。
性能表现
ZGC时延吊打所有其他收集器,吞吐量基本和parallel GC持平。