0.垃圾收集分类
- Young GC |
|
|---|---|
- Old GC |
只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式 |
- Full GC |
清理整个堆的 GC 事件,包括新生代、老年代、元空间等 。 |
- Mixed GC |
清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式。 |
1.垃圾收集器分类
| 收集器 | 串行OR并行 | 回收算法 | 作用区域 |
|---|---|---|---|
| Serial收集器 | 单线程 | 复制算法 | 新生代 |
| Parallel Scavenge收集器 | 多线程 | 复制算法 | 新生代 |
| ParNew 收集器 | 多线程 | 复制算法 | 新生代 |
| Serial Old收集器 | 单线程 | 标记整理算法 | 老年代 |
| Parallel Old收集器 | 多线程 | 标记整理算法 | 老年代 |
| CMS 收集器 | 多线程 | 标记清除算法 | 老年代 |
| G1收集器 | 多线程 | 标记整理,复制算法 | 新生代+老年代 |
1.1 7 款经典收集器与垃圾分代之间的关系

| 新生代收集器 | Serial、ParNew、Paralle1 Scavenge |
|---|---|
| 老年代收集器 | Serial Old、Parallel Old、CMS |
| 整堆收集器 | G1 |
1.2 垃圾收集器的组合关系

- 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial old、Serial/CMS、ParNew/Serial old、ParNew/CMS、Parallel Scavenge/Serial 0ld、Parallel Scavenge/Parallel 01d、G1;
- 其中Serial O1d作为CMs出现”Concurrent Mode Failure”失败的后备预案。
- (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK14中:弃用Paralle1 Scavenge和SerialOld GC组合(JEP366)
- (青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)
2. Serial GC回收器:串行回收
2.1 特性
- Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器。
Serial收集器采用复制算法、串行回收和”stop-the-World”机制的方式执行内存回收。
3. Serial Old 回收器
3.1 特性
Serial Old是运行在Client模式下默认的老年代的垃圾回收器。
- Serial Old收集器同样也采用了串行回收和”stop the World”机制,只不过内存回收算法使用的是标记-整理算法。
- Serial 0ld在Server模式下主要有两个用途:
- 与新生代的Parallel Scavenge配合使用
-
4.ParNew GC回收器:并行回收
如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
-
4.1 特性
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、”Stop-the-World”机制。
- ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。
- 自适应调节策略是ParNew收集器的一个重要区别。
5.Parallel Scavenge 回收器
5.1 概念
Parallel Scavenge收集器是Java虚拟机中垃圾收集器的一种。
又称为吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。
Parallel Scavenge是Java1.8默认的收集器,特点是并行的多线程回收,以吞吐量优先。5.2 特性
5.2.1 高吞吐
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)
(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy,会根据GC的情况自动计算计算 Eden、From 和 To 区的大小;
5.3 核心参数
5.3.1 控制最大垃圾收集停顿时间
-XX:MaxGCPauseMillis参数
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
5.3.2 直接设置吞吐量大小
-XX:GCTimeRatio
GCTimeRatio参数的值应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间
5.3.3 UseAdaptiveSizePolicy开关参数
-XX:+UseAdaptiveSizePolicy是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)
5.4 涉及参数
| -XX:MaxGCPauseMillis | 控制最大垃圾收集停顿时间 |
|---|---|
| -XX:GCTimeRatio | 直接设置吞吐量大小 |
| -XX:+UseAdaptiveSizePolicy | GC自适应调节 |
| -XX:-UseParallelGC | 将使用Parallel Scavenge(年轻代)+Serial Old(老年代)的组合进行GC |
6.Parallel Old 回收器
是Parallel Scavenge收集器的老年代版本,用于老年代的垃圾回收,但与Parallel Scavenge不同的是,它使用的是“标记-整理算法”。
6.1 涉及参数
| -XX:+UseParallelOldGC | 打开该收集器后,将使用Parallel Scavenge(年轻代)+Parallel Old(老年代)的组合进行GC。 |
|---|---|
7.CMS回收器:低延迟
7.1 特性
- 它第一次实现了让垃圾收集线程与用户线程同时工作。
- CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
- CMS的垃圾收集算法采用标记-清除算法,并且也会”stop-the-world”
7.2 回收过程
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记)。7.2.1 初始标记
初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。7.2.2 并发标记
并发标记(Concurrent-Mark)阶段:从Gc Roots的直接关联对象开始遍历整个对象图(GC
Roots Tracing)的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。7.2.2.1 三色标记法
GC 中的对象划分成三种情况:
| 白色 | 还没有搜索过的对象(白色对象会被当成垃圾对象) |
|---|---|
| 灰色 | 正在搜索的对象 |
| 黑色 | 搜索完成的对象(不会当成垃圾对象,不会被GC) |

这个图的意思就是:假设有 A -> B -> C, A 是 GC Roots 关联的对象,那么首先会把 GC Roots 标记,也就是 A 标记成灰色(证明现在正在搜索 A 相关的),然后搜索 A 的引用,也就是 B,那么搜索了 B,把 B 变成了灰色,那么 A 就搜索完成了。(此时注意,现在是不管 C 的,因为 C 不是 A 的引用,现在只管 A 的引用是什么)。此时把 A 相关的搜索完了,那么 A 就变成了黑色,证明 A 已经 ok 了。
Ps:浮动垃圾就是说,此时 A 又不用了,那么 A 是没办法回收的,因为 A 已经标记了)
此时准备要搜索 B 了。
刚好,此时,用户线程要执行了,用户线程把原来 A -> B -> C 的引用改成了 A -> C,同时 B 不再引用C。
然后又到 GC 线程执行了。
GC 线程发现 B 没有引用的对象了(因为用户线程已经把 B -> C 去掉了),那么 B 就相当于搜索完成了,变成黑色了。
最后,C 怎么办,C还是白色的呢,白色的是不会搜索,当做垃圾处理的。
解决办法
此时的解决办法就是有一个叫做写入屏障的东西。就是说,如果A已经被标记了(已经是黑色的了),那么用户线程改动 A->C的时候,会把 C 变成灰色,这样,以后就可以搜索 C了。
write_barrier(obj,field,newobj){if(newobj.mark == FALSE){newobj.mark = TRUEpush(newobj,$mark_stack)}*field = newobj}
这里的意思就是改的时候会 push(newobj,$mark_stack),
那么,通过这样子的一个方式,就能解决这个问题:GC 线程和 用户线程并发的时候,用户线程把失效的对象又至为有效,这个时候怎么处理。
7.2.3 重新标记
由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
7.2.4 并发清除
此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“stop-the-World”,只是尽可能地缩短暂停时间。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以CMS整体的回收是低停顿的。
7.3 缺陷
7.3.1 对系统吞吐量的影响
CMS收集器对CPU资源比较敏感。本来可以有10个用户线程处理请求,现在却要分出3个作为垃圾回收线程,吞吐量下降了30%,CMS默认启动的回收线程是(CPU数量+3)/ 4 , 如果CPU数量只有一两个,那吞吐量就直接下降50%,显然是不能接受的。一般生产级别的CPU 在8核左右, (8+3)/4=3 , 吞吐下降37.5%;
7.3.2 浮动垃圾-引发「Concurrent Mode Failure」问题
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。<br /> 要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败。JDK1.5 默认当老年代使用了68%空间后就会被激活。<br /> 当然这个比例可以通过降低阈值, -XX:CMSInitiatingOccupancyFraction 来设置,但是如果设置得太高很容易导致在CMS运行期间预留得内存无法满足程序要求,会导致Concurrent Mode Failure 失败,这时会启用后备预案 Serial Old 收集器来重新进行老年代的收集,而Serial Old 收集器是单线程,就会导致STW时间更长。
7.3.3 内存碎片化-停顿压缩
CMS收集器的垃圾收集算法采用的是**标记清除算法**,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。<br /> 如果无法找到足够大的连续空间来分配对象,将会触发Full GC。 当我们开启-XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的整理过程,内存整理会导致STW,停顿时间会变长。<br />还可以使用另外一个参数-XX:CMSFullGCsBeforeCompation用来设置执行多少次不压缩的FullGC后跟着来一次带压缩的。
8.G1回收器
8.1 分代模型
G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。G1相较之前其它的垃圾回收器,对模型进行了改变,不再进行物理分代,采用逻辑分代。它不再将连续内存分为Eden区和Old区,而是将内存分为一个个的Region。
一块Region(分区)在逻辑上依然分代,分为四种:Eden,Old,Survivor,Humongous(大对象,跨多个连续的Region)
8.1.1 Region 分区
G1把堆内存分成一块块的小内存分区, 每块分区的大小为1~32M之间。如果你不设置分区大小(+XX:G1HeapRegionSize = N),默认大小:X=Head(堆内存大小)/ 2048,但是X大小,只能在2的幂次方中取(1,2,4,8,16,32),所以最终大小就是X靠近那个2的幂次方,就为最终的值。
每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。
启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
8.1.2 Card 卡片

在每个分区内部又被分成了若干个大小为512 Byte 卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见 RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
8.1.3 Humongous
大对象区,存放超过阀值的大对象。(阀值 = G1HeadRegionSize / 2) 如果一个Object 超过了一个Region大小,那么就如上图所示,一个对象会放在几个连续的Region里面。
8.1.4 RSet
①. 问题:一个Region不可能是孤立的,一个Region中的对象可能被其他对象引用,如新生代中引用了老年代,这个时候垃圾回收时,会去扫描老年代,会出现STW
②. 解决:无论是G1还是分带收集器,JVM都是使用Remembered Set来避免全局扫描。每个Region都有一个对应的Remembered Set;
RSet:Remember Set ,记忆集合;
在串行和并行收集器中,GC 通过整堆扫描,来确定对象是否处于可达路径中。然而 G1 为了避免 STW 式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的 RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
事实上,并非所有的引用都需要记录在 RSet 中,如果一个分区确定需要扫描,那么无需 RSet 也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入 RSet 中;同时,G1 GC 每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在 RSet 中记录。最后只有老年代的分区可能会有 RSet 记录,这些分区称为拥有 RSet 分区(an RSet’s owning region)。
如上图所示,每个region都有一个记忆集(Rset),记忆集会记录下当前这个region中的对象被哪些对象所引用。例如,region2中的两个对象分别被region1中的对象和region3中的对象所引用,那么,region2的记忆集记录的就是region1和region3中的引用region2的对象的引用。
这样一来在回收region2的时候,就不用扫描全部的region了,只需要访问记忆集,就知道当前region2里面的对象被哪些对象所引用,判断其是不是存活对象。
8.1.4.1 Per Region Table (PRT)
RSet 在内部使用 Per Region Table(PRT)记录分区的引用情况。由于 RSet 的记录要占用分区的空间,如果一个分区非常”受欢迎”,那么 RSet 占用的空间会上升,从而降低分区的可用空间。G1 应对这个问题采用了改变 RSet 的密度的方式,在 PRT 中将会以三种模式记录引用:
- 稀少:直接记录引用对象的卡片索引
- 细粒度:记录引用对象的分区索引
- 粗粒度:只记录引用情况,每个分区对应一个比特位
由上可知,粗粒度的 PRT 只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。
8.1.4.2 Write Barrier 写屏障
就是对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑。
- 每次在对一个对象引用进行赋值的时候,会产生一个写屏障中断操作,
- 然后检查将要写入的引用指向的对象是否和该引用当前指向的对象处在不同的
region中; - 如果不同,通过
CardTable将相关的引用信息记录到Remembered set中; 当进行垃圾收集时,在
GC根节点的枚举范围内加入Remembered Set,就可以保证不用进行全局扫描。8.1.5 CSet
8.2 G1收集器优缺点
8.2.1 并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
8.2.2 分代收集
从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
8.2.3 空间整合
CMS:”标记-清除” 算法、内存碎片、若干次Gc后进行一次碎片整理;
G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-整理(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
8.2.4 可预测的停顿时间模型
即软实时 soft real time
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
8.3 回收过程
8.3.1 年轻代GC
8.3.2 老年代并发标记过程
8.3.2.1 初始标记
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短
8.3.2.2 并发标记
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。
8.3.2.3 最终标记
最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面。
需要停顿线程
8.3.2.4 筛选回收
在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
9.ZGC回收器
引用
https://blog.csdn.net/ThreeAspects/article/details/109078278
http://www.linkedkeeper.com/1511.html
https://blog.csdn.net/TZ845195485/article/details/118304807
https://blog.csdn.net/weixin_42500385/article/details/109314374
https://blog.csdn.net/FMC_WBL/article/details/107864334
https://blog.csdn.net/TofuCai/article/details/107620720
https://blog.csdn.net/qq_15965621/article/details/107899419
GC垃圾回收(3)- 三色标记算法 https://www.jianshu.com/p/5116a7acb866
https://blog.csdn.net/HaveFerrair/article/details/50959110
