一. ParNew + CMS
ParNew + CMS 的组合,是用 ParNew 进行年轻代垃圾收集,用 CMS 进行老年代垃圾收集
1. ParNew 青年代
ParNew 是使用多条线程并行执行垃圾回收的算法,会阻塞用户线程的执行,发生 Stop The World。它是除了 Serial 收集器外,唯一能够和 CMS 老年代收集器配合的。后者是 jdk 第一次实现了用户线程与 gc 线程同时工作的收集器。
2. CMS 老年代
CMS(Concurrent Mark Sweep)- 中文并发标记清除
收集器,是为了达到最短停顿时间为目标的收集器。从名字上看出,该收集器的2个特点:并发
和基于标记清除
。CMS 老年代 gc 分为4步
- 初始标记(initial mark):只标记 GC Root 直接关联的对象
- 并发标记(
concurrent
mark):进行 GC Root Tracing,找到所有存活对象和垃圾对象 - 重新标记(remark):由于上一步 gc 线程和用户线程都在执行,可能在标记过程中,用户线程修改了对象的引用,这一步需要修正找回被用户线程重新引用的垃圾对象,和被用户线程取消引用的存活对象。因此要触发 stop the world,停顿时间比初始标记长,但比并发标记短。
- 并发清除(
concurrent
sweep):不阻塞用户线程的清除标记对象
步骤中,带有 concurrent 的是 gc 线程可以和用户线程一起执行的,即 CMS 只有初始标记
和重新标记
阶段会触发 stop the world。尽管 CMS 通过最大程度的并行大大降低了停顿时间,但自身仍有3个不足:
- (1)CMS 收集器要运行在多核 CPU 上
CMS 收集器默认启动的 gc 线程个数为(nCpu + 3)/4
,所以在 cpu 核数大于4时,gc 线程所占 cpu 大于等于 25%
,cpu 核数越少,gc 线程个数所占 cpu 比重就越大,导致即使在并发阶段,用户的执行效率也会降低最少 25% - (2)CMS 无法处理浮动垃圾
由于 CMS 的垃圾清除阶段, gc 线程和用户线程也是一起执行的,这会导致在垃圾清除的过程中,用户线程有产生了新的垃圾对象,这些垃圾对象由于没有经过标记导致本次 gc 过程无法处理,因此称之为“浮动垃圾”。由于存在浮动垃圾,不能等到老年代沾满再出发 major gc,而是要预留一部分给并发清除阶段使用。1.6中默认老年代占满92%
触发 major gc,该比例也由参数-XX:CMSInitiatingOccupancyFraction
配置,该参数不能设置太高,否则老年代空间预留不足,导致并发清除阶段产生 “Concurrent Mode Failure
”,这会退化为 Serial 收集器回收,停顿时间就长了。 - (3)“标记清除”收集器会产生大量内存碎片
碎片过多,会给大对象分配带来麻烦,往往老年代还有很多空间剩余,但无法找到合适的连续空间分配大对象,不得不触发一次 full gc。为了解决这个问题, jvm 提供一个参数-XX:+UseCMSCompactAtFullCollection
,在因为找不到合适空间而进行 full gc 之前,先对老年代进行一次碎片整理,当然者仍然会增加停顿时间。
二. G1 收集器
G1 提供延迟和吞吐量之间的平衡, 有可预测的 gc 停顿时间。使用-XX:+UseG1GC
开启 G1 收集器。G1 可以理解为既能回收青年代,又能回收老年代 ,尽管在 G1 中已经没有“代”的概念。
g1 将内存化整为零
,取消 青年代, 老年代的空间划分, 取而代之的是,将整个堆划分成2048
个Region,每个Region的大小在1-32MB
之间,具体多大取决于堆的大小。这样可以不必担心原先每个代的内存是否足够。region 分为四种类型:
(1) Eden 区域
(2) Survior 区域
(3) old 区域
(4) 新加的 humongous 区域
所以 G1 其实仍然属于分代收集器, 只是这些代不是连续的内存空间。Region 是 G1 回收器一次回收的最小单元
。即每一次回收都是回收 N 个 Region。 N 是多少,主要受用户设置的期望停顿时间有关。每一次的回收,G1会选择有最多垃圾
的 Region 进行回收。
- 新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到 old 区域 或者 Survivor区域。G1收集器通过将对象从一个区域复制到另外一个区域的方式,完成清理工作. 而且可以在复制过程中完成堆压缩(至少是部分堆的压缩),这样也就不会有cms的内存碎片问题的存在( CMS 属于标记清楚收集器, 所以会有内存碎片)。
- CMS 中, 因为有大对象直接产生在老年代的设定, 导致如果有短期存在的大对象, 会导致老年代 gc , 产生 full gc. 为了解决这个问题, g1 增加了
humongous
区域, 如果对象的 size 超过可区域的一半, 则该对象直接产生在humongous
区域, 如果对象的 size 超过一个humongous
区域大小, g1 会去寻找连续的几个humongous
区域 , 这个操作可能产生 full gc
G1 的 gc 策略
G1提供了两种GC模式,Young GC 和 Mixed GC,两种都是 Stop The World(STW) 的。下面我们将分别介绍一下这2种模式。
1. Young GC
默认超过 XX:InitiatingHeapOccupancyPercent=45
进行 young gc (含义在下面的条有参数)
Young GC 主要是对 Eden 区进行 GC,它在 Eden 空间耗尽时会被触发。在这种情况下,Eden 空间的数据移动到 Survivor 空间中,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到年老代空间。Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。最终 Eden 空间的数据为空,GC停止工作,应用线程继续执行。
(1) RSet (remember set) 加速年轻代引用扫描
RSet 是一种记录对象之间应用关系的表, 可以加速对无用对象的扫描
- CMS: 老年代中记录老年代对象引用了哪些青年代对象:
在CMS中,也有RSet的概念,是在老年代
中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,仅仅需要扫描这一块区域,而不需要扫描整个老年代查找对象是否被引用。这是point-out
的做法. - G1: 在每个青年带 region 中, 记录引用了哪些老年代的对象:
在 G1 中,并没有使用 point-out . 因为每个 region 太小,region 数量太多,如果是用 point-out 的话,会造成大量的扫描浪费,有些根本不需要 GC 的 region 引用也扫描了。于是 G1 中使用 point-in 来解决。point-in 的意思是记录哪些 region 引用了当前 region 中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次 GC 时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
(2) Card Table
需要注意的是,如果引用的对象很多,记录引用地址的开销就会很大. 如果能记录1个连续的地址空间, 就可以用连续地址来记录多个引用对象, 减少引用地址的开销,在 G1 中又引入了另外一个概念,卡表(Card Table)。一个 Card Table 将一个 region 在逻辑上划分为固定大小的连续区域,每个区域称之为卡(card), 大小介于 128 到 512 字节之间。CardTable 通常为字节数组,由 Card 的索引(即数组下标)来标识每个 region 的空间地址。默认情况下,每个卡都未引用。当一个地址空间有引用时,这个地址空间对应的数组索引的值被标记为"0",即标记为脏引用,此外 RSet 也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的 Region 的起始地址,Value 是一个集合,里面的元素是 Card Table 的 Index。
2. mixed gc
mixed gc = 正常的新生区域 gc + 扫描线程标记好的 old 区域
gc步骤分为 2 步: 全局并发标记
(global concurrent marking) + 拷贝压缩存活对象
(evacuation)
全局并发标记分为 4 步:
上文中,多次提到了global concurrent marking,它的执行过程类似CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为四个步骤:
- 初始标记(initial mark,
STW
): 它标记了从GC Root开始直接可达的对象。 - 并发标记(Concurrent Marking): 这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
- 最终标记(Remark,
STW
)。标记那些在并发标记阶段发生变化的对象,将被回收。 - 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free list。
该阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。
第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
什么时候发生Mixed GC呢?由如下一些参数控制,这些参数也控制着哪些老年代 Region 会被选入 CSet。
G1HeapWastePercent
:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比
是否超过此参数,只有超过了
,下次才会发生Mixed GC。- G1MixedGCLiveThresholdPercent:old generation region中的
存活对象的占比
,只有在此参数之下,才会被选入CSet
。 - G1MixedGCCountTarget:一次 global concurrent marking 之后,最多执行 Mixed GC 的次数。
- G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入 CSet 的最多 old generation region 数量。
并发标记阶段
的三色标记法:
三色标记可以推演回收器的正确性。首先,我们将对象分成三种类型的。
(1) 黑色: root 对象,或者该对象与它的子对象都被扫描
(2) 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
(3) 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
当GC开始扫描对象时,按照如下步骤进行对象的扫描:
(1) 根对象被置为黑色,子对象被置为灰色。
(2) 继续由灰色遍历,将已扫描了子对象的对象置为黑色。
(3) 遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。 这个过程看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失
问题. 比如对象 c 已经被标记为无用对象, 但在并发标记过程中, 应用程序有吧对象 C 关联到对象 A 的属性 A.c = c
. 如何保证GC标记的对象不丢失呢?有如下2种可行的方式:
- 在
插入的时候
记录对象 - 在
删除的时候
记录对象
刚好这对应CMS和G1的2种不同实现方式:
- 在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
- 在G1中,使用的是
SATB
(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:- 在开始标记的时候生成一个堆的逻辑"快照" (对象引用图)
- 在并发标记的时候所有被改变的对象入队(在写引用之前加入write barrier,先将所有旧引用所指向的对象都变成非白的,等待最终标记)
- 可能存在游离的垃圾,将在下次被收集
这样,G1到现在可以知道哪些老的 region 可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
3. full gc for g1
full gc下的退化
某些情况下, g1 触发 full gc, 退化为使用单线程的 Serial 收集器完成垃圾清理:
- 并发模式失败
在进行 mixed gc 之前, old 区域就被沾满, 则 g1 会跳过并发标记周期. 这种情况下需要增加堆大小或调整周期. 例如增加线程数-XX:ConcGCThreads
等 - 晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在 gc 日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:- a. 增加
-XX:G1ReservePercent
选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。 - b. 通过减少
-XX:InitiatingHeapOccupancyPercent
提前启动标记周期。 - c. 也可以通过增加
-XX:ConcGCThreads
选项的值来增加并行标记线程的数目。
- a. 增加
- 巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize
,使巨型对象不再是巨型对象。
g1 如何达到限制 gc 停顿时间的目标
参数 XX:MaxGCPauseMillis=200
设置用户期望的 gc 停顿时间. G1怎么满足用户的期望呢?就需要这个停顿预测模型. 该模型通过历史数据, 预测本次 gc 要选择的 region 数量, 通过控制进行 gc 的 region 数量, 来满足用户期望. 在G1 GC 过程中,每个步骤花费的时间都记录其衰减均值、衰减变量,衰减标准偏差等, 最后根据一个公式预测本次 gc 要选择的 region 数量. 需要进行 gc 的 region 加入 CSet 中. (CSet是为了满足控制停顿时间而设计的筛选机和)
三. 调优参数对比
-
CMS
- 可以设置年轻带和老年代大小的比例
- 可以设置老年代占用多少就触发 magor gc,
- 可以设置 full gc 前开启压缩
-
G1
-
可以设置用户期望的停顿时间
-
设置 G1 每个 region 的大小。
-XX:G1HeapRegionSize=n
,值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标和默认数值是根据 Java 堆的大小划分出 2048 个 region -
设置 STW 阶段的 gc 线程数
-XX:ParallelGCThreads=n
,将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的5/8
左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的5/16
左右。 -
设置非 STW 阶段的 gc 线程数
-XX:ConcGCThreads=n
,将 n 设置为并行垃圾回收线程数(ParallelGCThreads) 的 1/4
左右。 -
-XX:InitiatingHeapOccupancyPercent=45
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
避免使用以下参数:
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。 -
四. CMS 与 G1 对比
CMS 虽然是最小停顿时间的收集器,但因为其
- 有内存碎片
- 并发清除对象无法处理浮动垃圾等问题
会导致意外的 full gc 情况出现,表现不稳定。 对于G1 收集器
- 没有内存碎片
- 停顿时间可控
- region 的化整为零取消了因为分代设置不合理导致的 gc
- mix gc 的全局并发标记 + STW 的拷贝压缩对象没有浮动垃圾