收集器是回收算法的实现。

概述

Serial和Serial Old垃圾回收器:分别用来回收新生代和老年代的垃圾对象
ParNew和CMS垃圾回收器:ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。
G1垃圾回收器:统一收集新生代 和老年代,采用了更加优秀的算法和设计机制,回收器分类

GC性能指标

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值
暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间
内存占用:java堆所占内存的大小
收集频率:垃圾收集的频次

JVM的痛点“Stop the World”问题

jvm在做垃圾回收的时候,还能继续往Eden区放对象吗? 不能的,在做垃圾回收的时候还有新对象往进放,垃圾回收器支持这样的操作是十分复杂的。
在垃圾回收的时候,尽可能要让垃圾回收器专心致志的干工作,不能随便让我们写的Java系统继续对象了,所以此时JVM会在后台直接进入“Stop the World”状态。会直接停止我们写的Java系统的所有工作线程,让我们写的代码不再运行!
image.png
假设我们的Minor GC要运行100ms,那么可能就会导致我们的系统直接停顿100ms不能处理任何请求,如果遇到内存分配不合理频繁进行Old GC,Old GC 一下就要好几秒,那整个系统就要停顿好几秒这样子用户体验感就会极差。
无论是新生代GC还是老年代GC,都尽量不要让频率过高,也避免持续时间过长,避免影响系统正常运行,这也是使用JVM过程中一个最需要优化的地方,也是最大的一个痛点。

Serial/SerialOld收集器

在这个收集器做回收的时候,新生代采用复制算法,老年代采用标记-整理算法
image.png
使用方式:-XX:+UseSerialGC

年轻代垃圾回收器

ParNew收集器

该收集器实质是serial收集器的多线程并行版本,充分利用多核cpu的性能,也是复制算法。
image.png
使用方式:-XX:+UseParNewGC
设置线程数: XX:ParllGCThreads,默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样

Parallel Scavenge收集器

它是一个新生代收集器,使用的复制算法,并且他也是一个多线程的收集器。看似和ParNew一样,不同的是 Parallel Scavenge的目标是达到一个可控制的吞吐量。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

提供两个参数控制吞吐量

1.控制最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
这个参数的值是一个大于0的值,表示收集器将尽可能地保证内存回收花费的时间不超过设定值。
GC停顿时间是以牺牲吞吐量和新生代空间来换取的,若是时间过小,那么一次GC不能将大部分对象清理的时候就会多次触发GC,哪怕每次GC的时间很短同时吞吐量也就降下来了
2.设置吞吐量大小:-XX:GCTimeRatio
值设置为大于0小于100的整数,也就是垃圾收集时间占总时间的比率。如果设置为19那么允许最大GC时间就占总时间的5%(1/(1+19))

-XX:+UseAdaptiveSizePolicy

该参数打开之后就不需要手工指定新生代的大少,以及Eden和Survivor区的比例、晋升老年代对象的大小等等参数。
先当于开启了GC自适应调节策略。
同时有一个Parallel Old收集器,该收集器也就是Parallel Scavenge的老年版本,负责收集老年代的对象

扩展

到底是用单线程垃圾回收好,还是多线程垃圾回收好? 到底是Serial垃圾回收器好还是ParNew垃圾回收器好?

启动系统的时候是可以区分服务器模式和客户端模式的,如果你启动系统的时候加入“-server”就是服务器模式,如果加入“-cilent”就是客户端模式。
如果系统是部署在4核8g的服务器上,那么就应该用服务器模式,如果是运行在windows、ios上那就是客户端模式。

服务器模式

服务器模式通常运行我们的网站系统、电商系统、业务系统、APP后台系统之类的大型系统,一般都是多核CPU。
所以此时如果要垃圾回收,那么肯定是用ParNew更好,因为多线程并行垃圾回收,充分利用多核CPU资源,可以提升性能。

客户端模式

如果你的Java程序是一个客户端程序,比如类似百度云网盘的Windows客户端,或者是印象笔记的Windows客户端。这种操作系统很多都是单核CPU,此时你如果要是还是用ParNew来进行垃圾回收,就会导致一个CPU运行多个线程,反而加重了性能开销,可能效率还不如单线程好。单CPU运行多线程会导致频繁的线上上下文切换,有效率开销。
image.png

老年代垃圾回收器

CMS收集器⭐

CMS(concurrent mark sweep),以获取最短回收停顿时间为目标的收集器,响应速度快,同时垃圾回收线程和系统工作线程尽量同时执行的模式来处理的,采用标记-清除算法。
image.png
有以下几个步骤要完成

标记过程

1.初始标记

暂停用户线程开始标记,时间很短
image.png
所谓的“初始标记”,他是说标记出来所有GC Roots直接引用的对象
举例:
image.png
这段代码中仅仅就将replicaManager给标记出来,而replicaFetcher是类的实例变量,因此是不能作为GCRoot的。
image.png

2.并发标记

系统线程可以随意创建各种新对象,继续运行。在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪。
意思就是对类似“ReplicaFetcher”之类的全部老年代里的对象,他会去看他被谁引用了。通过检查,发现ReplicaFetcher被ReplicaManager类变量所引用,此时可以认定“ReplicaFetcher”对象是被GC Roots间接引用的,所以此时就不需要回收他。
同时,系统程序会一直工作,可能会创建出来新对象,部分对象变为垃圾对象。
image.png
这个阶段就是对老年代所有对象进行GC Roots追踪,其实是最耗时的,由于和应用程序是并发的,这种耗时影响并不大

3.重新标记

由于第二阶段的时候,应用程序也在执行那么在这个阶段结束的时候仍然会有垃圾对象未被标记,所以此时进入第三阶段,要继续让系统程序停下来,再次进入“Stop the World”阶段。重新标记一下二阶段未标记的引用对象,把产生的垃圾对象取消引用。
这个过程是很快的,因为他只要标记一下二阶段产生的新对象即可。
image.png->image.png

4.并发清除

这个阶段就是让系统程序随意运行,然后CMS来清理掉之前标记为垃圾的对象即可。
image.png

缺点

CMS收集器对CPU资源非常敏感。

在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。并发标记的时候,需要对GC Roots进行深度追踪,看所有对象里面到底有多少对象是存活的。老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。
CMS默认启动的回收线程数是(处理器核心数量 +3) /4,也就是说, 如果处理器核心数在四个或以上, 并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源, 并且会随着处理器核心数量的增加而下降。 但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。 如果应用本来的处理器负载就很高, 还要分出一半的运算能 力去执行收集器线程, 就可能导致用户程序的执行速度忽然大幅降低。

CMS收集器无法处理浮动垃圾,可能出现”Concurrent Mode Failure”失败而导致另一次Old GC的产生。

在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象,但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是“浮动垃圾”。但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。
image.png
CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。CMS触发时机当老年代内存占用达到一定比例了,就自动执行GC。“-XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收。
要是CMS运行期间预留的内存无法满 足程序分配新对象的需要, 就会出现一次“并发失败”(Concurrent Mode Failure) , 这时候虚拟机将不 得不启动后备预案: 冻结用户线程的执行, 临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。

空间碎片:CMS是一款基于标记-清除算法实现的收集器,所有会有空间碎片的现象。

有空间碎片的话,存入大对象的时候直接进入老年代,由于连续的内存不足会导致触发Old GC,影响效率。
所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Old GC。
CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了。在Old GC之后要暂停工作线程,然后进行内存碎片整理,将存活的对象放到一起。
还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Old GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Old GC之后都会进行一次内存整理。

老版收集器参数实战

机器配置4核8g

年轻代垃圾回收参数设置

Survivor空间不够

对于普通业务,大部分对象都是短生命周期的,根本不应该频繁进入老年代,也没必要给老年代维持过大的内存空间,首先得先让对象尽量留在新生代里。这里就需要调整新生代和老年代的大小。
例如将新生代分2g老年代1g,from和to都是200mb这样就可以减少存活对象进入老年代的概率

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8”

image.png

调整对象年龄限制

这个参数考虑必须结合系统的运行模型来说,如果躲过15次GC都几分钟了,一个对象几分钟都不能被回收,说明肯定是系统里类似用@Service、@Controller之类的注解标注的那种需要长期存活的核心业务逻辑组件。那么他就应该进入老年代,何况这种对象一般很少,一个系统累计起来最多也就几十MB而已。
这种对象就尽快让他进入老年代,适当降低“-XX:MaxTenuringThreshold”参数的值(网上有一些说是要增加这个参数的大小,具体还是要根据业务实际来)

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5”

多大的对象直接进入老年代

在对象分配的时候,存在一个逻辑是“大对象直接进入老年代”。那如何考虑多大的对象放入老年代呢。
一般来说,给他设置个1MB足以,因为一般很少有超过1MB的大对象。如果有,可能是你提前分配了一个大数组、大List之类的东西用来放缓存的数据。

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M”

指定垃圾回收器

新生代使用ParNew,老年代使用CMS

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC - XX:+UseConcMarkSweepGC”

老年代垃圾回收参数设置

判断何时触发Old GC

1.每次Minor GC之前,都检查一下“老年代可用内存空间” < “历次Minor GC后升入老年代的平均对象大小”
2.老年代可用内存空间<新生代总对象大小
3.设置了“-XX:CMSInitiatingOccupancyFaction”参数,比如设定值为92%,那么此时可能前面几个条件都没满足,但是刚好发现这个条件满足了,比如就是老年代空间使用超过92%了,此时就会自行触发Old GC

尽量让Old GC过了高峰期之后触发。

是否要考虑Concurrent Mode Failure?
当然要考虑,但是由于cms清理很慢,必须要提高对存活对象的过滤性才行。通过对业务内存模型的分析,在考虑年轻代垃圾回收的时候就已经做了一个过滤了,好的过滤很难让无关垃圾对象进入老年代,因此在cms做并发清理的时候很难遇见Concurrent Mode Failure。即便是遇到这个异常了,在cms做清理的时候也是在业务高峰期下去了的时候做处理,是有一定影响但是不影响核心环节。

参数可以设置如下

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC - XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92

CMS垃圾回收之后进行内存碎片整理的频率应该多高

在CMS完成Old GC之后,一般需要执行内存碎片的整理,这个参数一般来说没有必要调整,在大促高峰期,Old GC可能也就1小时执行一次,然后大促高峰期过去之后,就没那么多的订单了,此时可能几个小时才会有一次Old GC。

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC - XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0

老版收集器的痛点

无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“Stop the World”现象,对系统的运行是有一定影响的。之后对垃圾回收器的优化,都是朝着减少“Stop the World”的目标去做的。

分析一下MinorGC为什么比Old GC快10倍

首先,不管是什么对象都要先放入Eden区,Eden区满了之后根据GCRoot的判断标准去标记存活对象。同时新生代存活对象是很少的,这个速度是极快的,不需要追踪多少对象。直接将存活对象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。MinorGC都是采用复制算法效率要比Old GC要高。
Old GC就很慢了,在标记的第2阶段并发标记的时候,需要对老年代所有的对象都进行追踪,这样就很慢了,况且触发Old GC时,老年代中可以当GCRoot的对象一定很多了,而且能留存这么久的对象的依赖关系也能复杂一些,做追踪自然就比较慢了。
在并发清理的时候是要找到零零碎碎的对象进行清除,速度也很慢。同时CMS垃圾回收期间预留的空间不够,新对象进来的还会抛出异常,随后暂停用户程序,临时启用Serial Old收集器来重新对老年代做跟踪清理。清理完成之后还要做一次内存碎片的整理操作,这里也要暂停一下工作线程。这一套操作下来Old GC慢的要死。

G1

G1用于解决什么场景下问题

G1的第一个重点是为运行需要大量堆且GC延迟有限的应用程序的用户提供解决方案。 这意味着堆大小约为6gb或更大,并且稳定且可预测的暂停时间低于0.5秒。 (类似Kafka、Elasticsearch之类的大数据相关的系统,都是部署在大内存的机器上的, 此时如果你的系统负载非常的高,对于大数据系统是很有可能的,比如每秒几万的访问请求到Kafka、Elasticsearch上去。)

G1的相关概念

G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,他一个人就可以搞定所有的垃圾回收。
将java堆内存分成一个个小的Region进行控制。
1.它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,为2的N次幂,即1MB, 2MB, 4MB, 8MB, 16MB,32MB
2.G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果一个对象存储超过1 .5个region,就放到H。一般被视为老年代.
image.png
G1也是有老年代和年轻代概念(逻辑上的)
image.png
G1最大的一个特点,就是可以让我们设置一个垃圾回收的预期停顿时间,例如:希望G1在垃圾回收的时候,可以保证在1小时内由G1垃圾回收导致的“stop the world”的时间不能超过1分钟。

G1的核心设计思路(对垃圾回收导致的系统停顿是可控的)

G1中存在一个叫回收价值的概念,

回收价值:

每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾

示例:

G1追踪发现,在这里面有10mb的垃圾是需要1s去做回收,另一个region中存在20mb垃圾,回收时间需要200ms。
image.png
G1会发现在最近一个时间段内,比如1小时内,垃圾回收已经导致了几百毫秒的系统停顿了,现在又要执行一次垃圾回收,那么必须是回收上图中那个只需要200ms就能回收掉20MB垃圾的Region。
image.png

总结

G1可以做到让你来设定垃圾回收对系统的影响,通过把内存划分成若干的region。追踪各个region,同时分析region中的垃圾大小和回收预估时间。最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。

Region可能属于新生代也可能属于老年代

刚开始Region可能谁都不属于,然后接着就分配给了新生代,然后放了很多属于新生代的对象,接着就触发了垃圾回收这个Region
image.png
随后剩余保留的对象有可能不会被移动,而是将当前这个region设置为拱老年代使用的region
image.png
所以其实在G1对应的内存模型中,Region随时会属于新生代也会属于老年代,所以没有所谓新生代给多少内存,老年代给多少内存这一说了

G1的内存模型和分配规则

G1的内存大小如何设定

默认情况下自动计算和设置的,可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。JVM在启动的时候是使用的G1垃圾回收器,使用“-XX:+UseG1GC”来指定使用G1,这个时候自动用堆大小除以2048。
jvm最多可以有2048个region,同时region的大小必须是2的倍数。例如有4g内存,除以2048后每个region就是2MB。如果通过手动方式来指定每个region的大小,则是“-XX:G1HeapRegionSize”。
image.png
默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个是可以通过“-XX:G1NewSizePercent”来设置新生代初始占比的,其实维持这个默认值即可。
JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”。

新生代还存在Eden和Survior吗?

G1中是存在这个概念的,同时还是可以用“-XX:SurvivorRatio=8”进行设置内存比例的。Eden和Survior只不过是占据了不同的region罢了,比如新生代之前说刚开始初始的时候,有100个Region,那么可能80个Region就是Eden,两个Survivor各自占10个Region。
image.png

G1的垃圾回收

随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%。这个时候就会触发新生代的GC,G1就会使用复制算法进行垃圾回收,进入一个“Stop the World”状态。随后将存活对象放入survivor区。
区别:但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。
image.png->image.png

对象什么时候进入老年代

这个标准和老版收集器基本一样
(1)对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就会进入老年代
(2)动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%。
比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代,这就是动态年龄判定规则
(3)空间担保规则
这里就是没有大对象直接分配给老年代,大对象有专门Region去管理。

大对象Region

G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。
大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中。
如果一个大对象过于大了,一个region放不下的时候,就会横跨多个region来存放。
image.png

G1垃圾回收

G1必懂的垃圾回收周期

垃圾回收轨迹可以按下图理解,这里分两部分一部分是Young-only,一部分是Space Reclamation。图中蓝色的圆圈标识是年轻代垃圾回收暂停、橙色圆圈表示标记引起的暂停、红色标识混合收集暂停。图中的存在的Old gen occupancy exceeds threshold也就是那个大一点的蓝色圆圈这个标记代表的是,当老年代对象占堆内存比例到一个阈值之后开始做一系列的标记(就是G1垃圾回收过程的标记=mixGC标记过程)。

  • Young-only:这个阶段从一些普通的年轻集合开始,将对象提升到老年代。当老年代占用率达到某个阈值,即这个阶段只有老年代的使用空间大小超过了InitiatingHeapOccupancyPercent参数设置的时候才开始并发标记,年轻代和空间回收阶段之间的过渡开始(G1垃圾回收标记过程)。
    • Concurrent Start:除了执行正常的年轻代垃圾收集之外,这种类型的收集还会启动标记过程(混合回收的标记过程)。并发标记决定保留老年代区域中当前可达(活动)的所有对象,以供后续空间回收阶段使用。当收集标记还没有完全完成时,可能会发生正常的年轻收集。
    • Remark:此暂停完成标记本身,执行全局引用处理和类卸载,回收完全空的区域并清理内部数据结构。在Remark和Cleanup之间,G1计算信息,以便以后能够同时回收选定老一代区域的空闲空间,这将在Cleanup暂停时完成。
    • CLeanup:这个阶段决定要不要进入空间回收阶段,如果接下来是空间回收阶段,则young-only阶段以单个Prepare Mixed young收集完成(标记出要回收的年轻代对象)。(这个阶段并不会实际上去做垃圾的收集)
  • Space Reclamation:这个阶段由多个混合集合组成,除了清理年轻代区域外,还清除老年代区域集的活动对象。当 G1 确定清除了更多的老年代区域对象并且不会产生足够的可用空间时,空间回收阶段结束。

垃圾收集器(算法的实现) - 图26

什么时候触发新生代+老年代的混合垃圾回收

https://blog.csdn.net/lovejj1994/article/details/109620239(XX:InitiatingHeapOccupancyPercent参数)
通过这个参数控制“-XX:InitiatingHeapOccupancyPercent”,默认值是45%
老年代占据了整堆内存的45%的Region的时候,此时就触发并发标记阶段,这个阶段完成了就可以开始清理了。

G1垃圾回收的过程

这个回收的过程就是G1对对象做标记的周期 根据官方文档描述,G1整个过程就是一个标记周期,周期中每个阶段要做不同的事情,同时G1在标记前和标记完成会有特定的标识表明这一阶段结束了

Phases of the Marking Cycle

The marking cycle has the following phases:

  • Initial marking phase: The G1 GC marks the roots during this phase. This phase is piggybacked on a normal (STW) young garbage collection.G1 GC在此阶段标记根。 这个阶段依赖于一个正常(STW)的年轻垃圾收集。
  • Root region scanning phase: The G1 GC scans survivor regions marked during the initial marking phase for references to the old generation and marks the referenced objects. This phase runs concurrently with the application (not STW) and must complete before the next STW young garbage collection can start.G1 GC扫描在初始标记阶段标记的幸存者区域,以获取对老年代的引用,并标记被引用的对象。 此阶段与应用程序(不是STW)并发运行,必须在下一次STW年轻垃圾收集开始之前完成。
  • Concurrent marking phase: The G1 GC finds reachable (live) objects across the entire heap. This phase happens concurrently with the application, and can be interrupted by STW young garbage collections.G1 GC在整个堆中找到可到达的(活动的)对象。此阶段与应用程序同时发生,并且可以被STW年轻垃圾收集中断。
  • Remark phase: This phase is STW collection and helps the completion of the marking cycle. G1 GC drains SATB buffers, traces unvisited live objects, and performs reference processing.这个阶段是STW的标记,帮助完成标记周期。G1 GC耗尽SATB缓冲区,跟踪未访问的活动对象,并执行引用处理
  • Cleanup phase: In this final phase, the G1 GC performs the STW operations of accounting and RSet scrubbing. During accounting, the G1 GC identifies completely free regions and mixed garbage collection candidates. The cleanup phase is partly concurrent when it resets and returns the empty regions to the free list.在这个最后阶段,G1 GC执行记帐和RSet擦洗的STW操作。 在统计时,G1 GC会标识完全自由的区域和混合垃圾收集候选者。 这个清理阶段在重置并将空区域返回给空闲列表时部分并发。

初始标记

这个过程是需要进入“Stop the World”的,标记一下GC Roots直接能引用的对象,这个过程速度是很快的。
停止工作线程,对线程栈中局部变量代表的GCRoots,以及方法区中类静态变量代表的GCRoot进行扫描,做一个标记。
image.png

并发标记

这个个,会将所有被GCRoot间接引用的对象标记下来,这种间接引用的对象也要存活下来。这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。但是这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大。
而且JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。
如果找到空白区域(如“ X”所示),则在Remark阶段将其立即删除。另外,计算确定活跃度的信息。
垃圾收集器(算法的实现) - 图28

最终标记

这个阶段会进入STW,会根据并发标记 阶段记录的那些对象修改,通过(SATB)算法最终标记一下有哪些存活对象,有哪些是垃圾对象。
SATB:G1使用一种称为初始快照(SATB)的技术来保证垃圾回收器能找到所有活动对象。在并发标记(整个堆上的标记)开始时处于活动状态的任何对象都被认为是处于活动状态的SATB允许以类似于CMS增量更新的方式处理浮动垃圾。
image.png

注意:并发标记阶段会带来2个问题 1)浮动垃圾 2)漏标

筛选回收阶段/复制清理阶段

这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。随后停止系统程序,然后开始做垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。(根据以前收集的数据,G1估计在目标时间内可以收集多少个区域。 因此,收集器有一个相当精确的区域生成一个成本模型,它使用这个模型来确定在暂停时间目标范围内收集哪些区域和多少区域。 )
此时不仅回收老年代,同时还回收新生代,在新生代、老年代、大对象区域中对Region的回收价值 和成本进行排序,在根据用户期望的GC停顿时间来制定回收计划G1选择“活度”最低的区域,保证用指定的时间(比如200ms)回收尽可能多的垃圾,并在这个过程中压缩和释放内存,这就是所谓的混合回收。CMS(并发标记扫描)垃圾收集不进行压缩。 并行压缩只执行整个堆压缩,这会导致大量的暂停时间。
image.png

回收失败时触发的GC

在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去。当拷贝的过程发现没有region了,回收就失败了。
一旦失败,立马就会停止工作线程,随后采用单线程标记、整理和压缩,空闲出一批region去承载回收后的对象,这个过程也很慢。

并发标记阶段的问题

CMS和G1都会有这些问题

并发标记

通过三色标记来记录一个对象的引用状态
白色:尚未访问过。
黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
灰色:本对象已访问过,但是本对象 引用到 的其他对象尚未全部访问完。全部访问后,会转换为黑色。
image.png
当标记完之后,就会对白色的集合做清除
由于这一步同时还有用户线程并发的做一些事情,那么就会产生两种问题 多标、漏标。

多标(不应该被标记成什么颜色,反而给标上了)

image.png
标记完成后,用户线程将D和E之间的引用给断开了,那么此时的E应该不能被标记成灰色的。这种情况只能等待下一轮GC将E清除掉了。这种对象称为:浮动垃圾

漏标(应该标记成什么颜色的对象反而没有被标记上)G1和cms对最终标记有很大区别

标记完成后。当用户线程让G和D关联上时,此时G并没有被标记成灰色,因此G也就被放到白色的队列中去了,这种影响将会很大。(多存在可以,缺少对象不行)

  1. 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象
  2. 并发标记时,应用线程删除所有灰色对象到该白色对象的引用,也就是赋值为NULL

image.png
解决方法:在用户线程做对象引用的时候,将这个对象当做灰色的记录下来,等待下一步,也就是重新标记的时候再次做一下可达性分析就可以保证不会被漏标,而且重新标记这一步也没有其余用户线程来干扰。

CMS和G1如何解决漏标的

多标实际上没有什么太大的问题,无非就是浮动垃圾多了点,可能会造成GC频繁,不过几率应该是很小的。通过一些参数优化,是可以规避的。但是漏标是问题严重的,如果新增对象更改对象引用没有及时做GCRoot的跟踪那么程序就会出错。
如何解决漏标问题:
image.png
image.png
发生漏标是在两个基础上:1、A对D的新增引用 。 2、B对D的引用遗弃。
1、关注引用的增加,当A指向D的时候,将A置为灰色,下次扫描(remark)的时候就可以通过A的重新扫描标记到D了了,就不会发生漏标了,CMS就是使用的这种方法。
2、关注引用的删除,每当发生删除时,会把被删除的这个引用推到GC的堆栈里,当下次并发标记扫描的时候,
会从这个堆栈里检测一下堆栈里的这些引用通过查看各自的rememberedset中是否有新的引用重新指向了自身,
如果有的话,将不把这个对象当作是垃圾对象。G1就是使用的这种方式
cms解决方案:
cms偏向于尽可能多的进行漏标补偿,以减少浮动垃圾的增加。使用Incremental update算法,致力于第一个条件的打破,记录所有新增的引用关系:利用Post-write barrier记录引用关系新增的变化,只要在写屏障(post-write barrier)里发现要有一个白对象的引用被赋值到一个黑对象的字段里,那就把这个黑对象变成灰色的,也就是需要把原本已经扫描过的GC Root和年轻代对象再扫描一遍。哪怕删除所有灰对象到该白对象的引用,remark 阶段重新以这些引用关系为根再扫描一遍就不会漏标了。例如:B->D 改为 B->C,C对象为新增,就要从B再扫描一遍,那么D的引用被删除了也是能知道的,因此也能处理部分浮动垃圾,在这种情况下。
incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root。(重新扫描所有GC Root十分耗时,但是浮动垃圾不会增太多)
G1解决方案:G1选择减少漏标补偿,代价是增加浮动垃圾。使用的是SATB(snapshot-at-the-beginning)(pre-write barrier)的方式,致力于第二个条件的打破。记录每一次的对象快照,快照中的每个对象都被认为是存活的。当B->D的引用改为B->C,通过write barrier写屏障,会将B->D和B->C的引用变更记录记录在satb_mark_queue中,当作是存活对象,从而可以保证D还是能够被标记为活的。相对于D来说,引用它的源由原来的B遍为了A,SATB是从源头来解决的,如果把B=null,虽然D成了永久垃圾,但是SATB本次GC还是会认为D是活的,不回收成为浮动垃圾,直到下次GC。(因此 G1 的 SATB 会产生更多的浮动垃圾。但是换来的好处就是:不需要像 CMS 那样 remark,再走一遍 root trace 这种相当耗时的流程。直接扫描satb_mark_queue就行了)
G1为何使用SATB算法:
1、G1的每个region中都有一个RSet,这个RSet存储的都是引用本region对象的引用,所有获得SATB只需要读region的RSet就可以获得。当灰色B>白色D引用消失时,引用D会被push到堆栈(satb_mark_queue),下次并发标记扫描时是可以拿到这个引用D的,由于有RSet的存在,不需要扫描整个堆去查找指向白色D的引用,效率比较高。
2、remark阶段SATB算法更快,原因是:remark阶段SATB算法只记录那些被删除的引用,即并行标记开始引用,后面不再引用的对象。cms就是要重新扫描那些GC Root包括其间接引用的对象。也就是为什么说G1适用于那种需要控制stw时间,并且要是大内存的应用程序了。一个是浮动垃圾多,第二个是为了要求stw时间够短必须要让GC更快。空间换时间的一个理念。

G1的收集模式:

  • Young GC:收集年轻代里的Region。
  • Mixed GC:年轻代的所有Region + 全局并发标记阶段选出的收益高的Region。
  • 无论是Young GC还是Mixed GC都只是并发拷贝的阶段。
  • 分代G1模式下选择CSet有两种子模式,分别对应Young GC和Mixed GC:
    Young GC:CSet就是所有年轻代里面的Region。
    Mixed GC:CSet是所有年轻代里的Region加上在全局并发标记阶段标记出来的收益高的Region。
  • G1的运行过程是这样的:会在Young GC和Mixed GC之间不断地切换运行,同时定期地做全局并发标记,在实在赶不上对象创建速度的情况下使用Full GC(Serial GC,也就是从G1会回收到备选的方案,一定要尽量避免此情况出现)。
  • 初始标记是在Young GC上执行的,在进行全局并发标记的时候不会做Mixed GC,在做Mixed GC的时候也不会启动初始标记阶段。
  • 当Mixed GC赶不上对象产生的速度的时候就退化成Full GC,这一点是需要重点调优的地方。

    G1垃圾回收器的一些参数

    混合回收次数“-XX:G1MixedGCCountTarget”默认8

    在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8。
    也就是说,在最后回收阶段的时候,混合回收一些Region,再恢复系统运行,接着再次回收。
    多次回收的目的:停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。这个过程特别像单线程并发的情况,交替执行其实效率能好一点。(周期途中没有圆点标的地方就是项目运行的时候。)

    暂停混合回收“-XX:G1HeapWastePercent”默认10%

    在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉
    image.png
    接下来原来的Region里面的东西就没有了,当可回收的垃圾的占比小于设定的这个值,就停止混合回收。同时这种清理方式也不会存在内存碎片,也不用像cms一样做内存碎片管理。

    “-XX:G1MixedGCLiveThresholdPercent”默认65%

    确定要回收的Region的时候,必须是存活对象低于Region大小65%的对象才可以进行回收。如果大于65%就不用回收了,这种数据太大拷贝的时候成本也很高。

    G1案例

    背景

    百万级注册用户的在线教育平台。压力在上课功能。这个功能中主要提供互动的操作。
    比如说晚上3小时高峰期内有总共60万活跃用户,平均每个用户大概会使用1小时左右来上课,那么每小时大概会有20万活跃用户同时在线学习。这20万活跃用户因为需要进行大量的互动操作,所以大致可以认为是每分钟进行1次互动操作,一小时内会进行60次互动操作。那么20万用户在1小时内会进行1200万次互动操作,平均到每秒钟大概是3000次左右的互动操作。
    一般系统的核心服务需要部署5台4核8G的机器来抗住是差不多的,每台机器每秒钟抗个600请求,这个压力可以接受,一般不会导致宕机的问题。
    假设每次请求5kb的内存对象,一秒平均到一台机器上的请求就是600,因此就有3MB的对象内存占用。

G1内存默认布局

“-Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC”

分配4g给堆内存,新生代默认初始占比为5%,最大占比为60%,同时线程栈为1m,方法区为256M
按照默认的分配,每个region的大小=4096/2048,每个region就是2m,新生代占据5%的Region,100个左右。也就是200Mb的空间
image.png

分析多久触发新生代GC

按照一秒3MB的速度存入,同时“-XX:G1MaxNewSizePercent”参数限定了新生代最多就是占用堆内存60%的空间。

“-XX:G1MaxNewSizePercent”理解误区

“必须得随着系统运行一直给新生代分配更多的Region,直到新生代占据了60%的Region之后,无法再分配更多的Region了,再触发新生代gc”,这个观点是不对的。
原因:按照一秒3MB的速度存入,大概1分钟新生代的region就存满了,然后继续扩大新生代的region不断让对象进来,直到达到60%的内存再去GC。此时就会出现一些情况,由于“-XX:MaxGCPauseMills”控制,导致一次GC清理不完而去多次进行GC。而且G1是根据region的回收价值去选择回收哪些region的,有可能几次gc过去“-XX:MaxGCPauseMills”设置的暂停时间已经被用完了,仍然有好个region的东西还清不掉,那就进入老年代的几率就很大了。同时,对大面积region做GC扫描压力也很大还要考虑“-XX:MaxGCPauseMills”参数的控制,压力都集中在一个范围中了。这样就不可取。
实际上G1是一个动态调整的一个收集器:
当新生代的Region的被填满了,G1觉得要是现在就触发一次新生代gc,那么回收区区200MB只需要大概几十ms,最多就让系统停顿几十ms而已。和“-XX:MaxGCPauseMills”限定的时间相差很远。但是这样的话可能会造成GC十分频繁。因此可能就会先给新生代先多分配几个region,之后再去动态调整触发GC的时间。
这里要明白就是说,G1它是动态去GC的,会根据你预设的gc停顿时间,给新生代分配一些Region,然后到一定程度就触发gc,并且把gc时间控制在预设范围内,尽量避免一次性回收过多的Region导致gc停顿时间超出预期。具体的分析要通过别的工具再去分析才能知道。
image.png ->image.png

新生代GC优化

按照上面的分析,G1主要根据“-XX:MaxGCPauseMills”动态调整触发GC。
“-XX:MaxGCPauseMills”过于小,G1一旦发现你对几十个Region占满了就立即触发新生代gc,然后gc频率特别频繁,虽然每次gc时间很短。
“-XX:MaxGCPauseMills”过于大,G1会允许你不停的在新生代理分配新的对象,然后积累了很多对象了,再一次性回收几百个Region,这样就出现了上面分析的“GC压力集中在一段时间内了”
具体的分析要通过系统压测工具、gc日志、内存分析工具结合起来进行考虑分析才能知道。

mixed gc如何优化

想一下与触发mixed gc相关的参数也只有“-XX:InitiatingHeapOccupancyPercent”。
那么这里优化思路大致和parNew+cms差不多,主要都是去控制对象不要频繁进入老年代。G1优化起来就比较方便了,只用调节“-XX:MaxGCPauseMills”是一个合适的值,让G1在何时的时间去对新生代gc就行。

常用参数

  1. 堆设置
    • -Xms:初始堆大小
    • -Xmx:最大堆大小
    • -Xmn:新生代大小
    • -XX:NewRatio:设置新生代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3
    • -XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:为3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5
    • -XX:MaxTenuringThreshold:设置转入老年代的存活次数。如果是0,则直接跳过新生代进入老年代
    • -XX:PermSize-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前)
    • -XX:MetaspaceSize-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)

image.png