上一节主要介绍了新生代的 Serial / PraNew / Parallel Scavenge 三种垃圾回收方法和老年代的serial old 和 paralle old收集器,本节主要介绍CMS和G1垃圾收集器。
目录
CMS收集器-标记整理算法
收集过程
CMS 产生的问题
G1(Garbage First)收集器
收集过程
G1特点
简单调优
区域(Region)
对象的分配策略
G1的垃圾收集模式
G1问题
注意点
总结
CMS(Concurrent Mark Sweep)收集器的设计目标是:获取最短回收停顿时间的收集器。
HotSpot在JDK1.5
推出的第一款真正意义上的并发(Concurrent)收集器; 第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。目前很大一部分的Java应用集中在B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。而这也恰恰是CMS所擅长的地方。
CMS的垃圾收集一共需要经过四个阶段:
优点 | 并发收集,此阶段比较耗时;由于采用并发收集可以减少停顿时间; |
缺点 | (1)由于和用户线程一起工作,CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,出现cpu资源的竞争; (2)CMS收集器无法处理浮动垃圾,可能出现“promotion fail -> Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活; (3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC; (4) 产生2次STW; |
使用场景 | 重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。牺牲吞吐量来获取较小的暂停时间。 |
参数使用 |
CMS 的 GC 日志 就是 CMS。 |
增量式并发收集器
为了解决缺点(1)cpu敏感的问题,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显。实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明为“deprecated”,即不再提倡用户使用。
CMS并行GC是大多数应用的最佳选择,然而, CMS并不是完美的,在使用CMS的过程中会产生2个最让人头痛的问题:
问题解析: 第一个问题promotion failed是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年带有足够的空闲空间,但是由于碎片较多,这时如果新生代要转移到老年带的对象比较大,所以,必须尽可能提早触发老年代的CMS回收来避免这个问题(promotion failed时老年代CMS还没有机会进行回收,又放不下转移到老年带的对象,CMS运行期间预留的内存无法满足程序其他线程需要,因此会出现下一个问题concurrent mode failure,从而回退到:stop-the-wold GC- Serail Old)。
尽管CMS使用一个叫做分配担保的机制,每次Minor GC之后要保证新生代的空间survivor + eden > 老年带的空闲空间,但是对象分配是不可预测的,总会有写对象分配在老年带是满足不了的。
这个问题的直接影响就是它会导致提前进行CMS Full GC, 尽管这个时候CMS的老年带并没有填满,只不过有过多的碎片而已,但是Full GC导致的stop-the-wold是难以接受的。
导致以上两个问题的主要原因是:空间不够,或者是因为空间总量够,但是由于碎片导致了没有连续的大的存储空间。
对于问题“promotion failed” 解决办法:
对于问题“concurrent mode failure” 解决办法:
这是一款兼顾新生代和老年代垃圾收集器,是JDK1.7提供的一个新的面向服务端应用的垃圾收集器,用于取代CMS垃圾回收。
G1收集器的运行步骤主要分为以下4步:
1. 初始标记(Initial Marking)
该阶段会STW。扫描根集合,仅标记一下GC Roots能直接关联到的对象,将所有通过根集合直达的对象压入扫描栈,等待后续的处理。在G1中初始标记阶段是借助Young GC的暂停进行的,不需要额外的暂停。虽然加长了Young GC的暂停时间,但是从总体上来说还是提高的GC的效率。
2. 并发标记(Concurrent Marking)
该阶段不需要STW。这个阶段不断的从扫描栈中取出对象进行扫描,将扫描到的对象的字段再压入扫描栈中,依次递归,直到扫描栈为空,也就是说trace了所有GCRoot直达的对象。同时这个阶段还会扫描SATB write barrier所记录下的引用。此步比较耗时,但是和应用程序一起工作,并发执行,和CMS一样,效率提高。
3. 最终标记(Final Marking)
这个阶段会STW,最终标记主要为了找出程序在并发标记期间因用户程序继续运作而发生变化的对象。这个阶段会处理在并发标记阶段write barrier记录下的引用,同时进行弱引用的处理。这个阶段与CMS的最大的区别是CMS在这个阶段会扫描整个根集合,Eden也会作为根集合的一部分被扫描,因此耗时可能会很长。
4. 筛选回收(Live Data Counting and Evacuation)
该阶段会STW。清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,这个阶段并不会实际上去做垃圾的收集,只是去根据停顿模型在CSet选出任意多个Region作为垃圾收集的目标,等待evacuation阶段来回收。筛选就是依据用户设置【-XX:MaxGCPauseMillis 】的允许的GC时长,在Cset里对排序的各个Region的回收价值和成本预估,控制GC停顿时间来制定回收计划,达到用户的期望;
G1的设计原则就是简单可行的性能调优,开发人员仅仅需要声明以下参数即可:
-XX:+UseG1GC -Xmx4g -XX:MaxGCPauseMillis=200
如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可,简单方便。G1将新生代,老年代的物理空间划分取消了。
G1里面的Region的概念不同于传统的垃圾回收算法中的分区的概念,但是仍然保留里分代的思想。G1默认把堆内存分为1024个分区,后续垃圾收集的单位都是以Region为单位的,仍然属于分代收集器。Region是实现G1算法的基础,每个Region的大小相等,通过-XX:G1HeapRegionSize参数可以设置Region的大小,这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
从图8中可以看出各个区域逻辑上并不是连续的。并且一个Region在某一个时刻是Eden,在另一个时刻就可能属于老年代。G1在进行垃圾清理的时候就是将一个Region的对象拷贝到另外一个Region中。
Humongous区域
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
对象的分配策略分为3个阶段:
TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。对TLAB空间中无法分配的对象 -> JVM会尝试在Eden空间中进行分配 -> 如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
Young GC
工作方式:Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
Mixed GC
如名字一样,“混合收集” 不仅进行正常的新生代垃圾收集,同时也回收线程标记的老年代分区。它的GC步骤主要分2步:
(1)全局并发标记(global concurrent marking),在G1 gc之前,会进行全局标记;
(2)拷贝存活对象(evacuation),排序预估回收;
具体运行步骤可参考图1。下面我们来看一下在G1工作的过程中的几个重要的问题。
1、如果仅仅是GC回收新生代对象,如何解决不同Region区域的引用,如何找到所有的根对象呢?
在垃圾回收的时候都是从Root开始搜索,这会先经过年轻代再到老年代,对于年轻代引用老年代的这种跨代不需要单独处理。但是老年代引用年轻代的会影响young gc,这种跨代需要处理。这里CMS和G1都用到了Card Table,一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡,卡通常较小,一个字节对应一个Card。当一个Card上的对象的引用发生变化的时候,就将这个Card对应的Card Table上的状态置为dirty,young gc的时候扫描状态是dirty的Card即可,CMS的老年代就会记录这样一个Card tbale。对于G1垃圾收集,又引入了Rset(Remembered Set),在老年代中有一块区域用来记录指向新生代的引用。 无论G1还是其他分代收集器,JVM都是使用Card table来避免全局扫描。
我们知道G1垃圾收集器将内存分为了不同的Region区域,不再以严格的年轻代和老年代来区分内存并进行垃圾回收,如果需要扫描整个old区,势必会浪费很多的时间,且扫描了一些不必要的Region区域。 G1通过RSet,每个Region中都有一个RSet,记录的是其他Region中的对象引用本Region对象的关系,是一种point-in的关系,即:谁引用了我的对象。因为G1分了很多Region,需要回收那个区域的时候,只需要判断要回收的区域是否有其他对象引用了该区域里的对象,即只需要找待回收区域的根对象即可,避免无效扫描。若存储point-out关系,将会扫描很多无关的Region区,造成时间性能的浪费。这里面还有另外一个集合:Collection Set,简称:CSet,CSet记录的是GC要收集的Region的集合,CSet里的Region可以是任意代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。如果Rset集合的引用对象较多,这里为了提高引用对象的查找和赋值处理问题,又通过卡表(Card Table)来实现查询和赋值,一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。RSet其实是一个Hash Table,Key是别的调用方Region的起始地址,Value是一个集合,里面的元素是Card Table的Index分区的地址。如下图9所示:
如上图所示,要回收年轻代的region A,只需要扫描C,D,F 区域的根对象即可,而不需要扫描整个old区。
分代G1模式下选择CSet有两种子模式,分别对应YoungGC和mixedGC:
2、如何解决对象在GC过程中分配的问题呢?
初始快照算法:snapshot-at-the-beginning (SATB),SATB是维持并发GC的一种手段。G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图。在GC收集的时候,新产的对象认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。
如何找到在GC的过程中分配的对象呢?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象。
3、在GC过程中引用发生变化的问题怎么解决呢?
三色标记算法
在并发标记中,通过三色标记法来完成对对象是否存活以及追踪的记录。比如我们定义三种颜色并赋予以下的意义:
如果在GC运行中,对象的引用关系发生来如下的变化
如图11所示,gc运行过程中,C对象的引用关系发生来改变,D引用C显然按照三色标记法C为白色是要被清理的,显然不太合理。所以这里需要记录此种改变。
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的,即插入的时候记录下来。
write_barrier(obj,field,newobj){
if(newobj.mark == FALSE){
newobj.mark = TRUE
push(newobj,$mark_stack)
}
*field = newobj
}
在G1中,通过Write Barrier就可以了解到哪些引用对象发生了什么样的变化,删除的时候记录所有的对象,它有3个步骤:
(1) 在开始标记的时候生成一个快照图SATB标记存活对象;
(2) 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的);
(3) 可能存在游离的垃圾,将在下次被收集;
4、可预测的停顿
G1记录跟踪了各个Region获取垃圾收集的价值大小,在后台维护一个优先列表;每次根据用户设置的允许的收集时间,优先回收价值最大的Region,可以有计划的避免全区的垃圾收集,这也是Garbage-First的由来,也是与CMS最大的区别;这就保证了在有限的时间内可以获取尽可能高的收集效率;
5、什么情况下会发生fullgc?
导致CMS FullGC的原因有两个:
1. Promotion Failure
在年轻代晋升的时候老年代没有足够的连续空间容纳,很有可能是内存碎片导致的。
2. Concurrent Mode Failure
在并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。
导致G1 Full GC的原因可能有两个,与CMS类似:
1. Evacuation的时候没有足够的to-space来存放晋升的对象;
2. 并发处理过程完成之前空间耗尽
G1的初衷就是要避免Full GC的出现,Full GC会会对所有region做Evacuation-Compact,而且是单线程的STW,非常耗时间。
其他:
system.gc()调用;
永久代空间不足,注意动态代理,反射,常量等用的比较多的服务;
6、Full-GC的影响?
(1)不断调优暂停时间指标
通过XX:MaxGCPauseMillis=x可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置。一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。
(2)不要设置新生代和老年代的大小
G1收集器在运行的时候会调整新生代和老年代的大小。通过改变代的大小来调整对象晋升的速度以及晋升年龄,从而达到我们为收集器设置的暂停时间目标。设置了新生代大小相当于放弃了G1为我们做的自动调优。我们需要做的只是设置整个堆内存的大小,剩下的交给G1自己去分配各个代的大小。
G1的运行过程是这样的,会在Young GC和Mix GC之间不断的切换运行,同时定期的做全局并发标记,在实在赶不上回收速度的情况下使用Full GC(Serial GC)。初始标记是搭在YoungGC上执行的,在进行全局并发标记的时候不会做Mix GC,在做Mix GC的时候也不会启动初始标记阶段。当MixGC赶不上对象产生的速度的时候就退化成Full GC,这一点是需要重点调优的地方。
优点 | G1主要用来取代CMS垃圾收集器,优点是: 1、简单:开发者控制调优变的简单; 2、并行与并发:充分利用cpu,减少STW的时间; 3、可预见性:可预测的停顿模型,G1可选取部分区域进行回收,可以缩小回收范围,控制减少全局停顿; 5、划分模式:堆内存的划分Region; 6、大空间分配:超大堆的表现更出色; |
缺点 | 会产生3次STW,但是时间较短; |
使用场景 | Java 9 的默认垃圾收集器,该收集器和之前的收集器大不相同,该收集器可以工作在young 区,也可以工作在 old 区。 |
参数使用 |
|
CMS与G1的区别:
(1) 分代收集 重新标记过程 和 回收方式(可预测回收模型)的不同。 |
参数的设定及垃圾回收器的选择一定要根据具体的服务及场景来判断选择,没有完美的唯一解决方案。比如C端服务器交互密集型,需要保证吞吐量,我们可以选择G1 或者 “吞吐量优先”的 Parallel Scavenge + Parallel Old,还有
PreNew + CMS的组合也是系统常用的,兼顾了吞吐量和停顿时间的性能考虑,
或者是:
Parallel Scavenge + old serial
,对于一般请求量不大,不要求实时性的单核cpu系统也可以采用 Serial + Serial old 即可满足需求,效率也很高。
备注:常用参数
JVM常用配置参数
配置参数 | 功能 |
---|---|
-Xms | 初始堆大小。如:-Xms4g |
-Xmx | 最大堆大小。如:-Xmx4g |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 永久代(方法区)的初始大小 |
-XX:MaxPermSize | 永久代(方法区)的最大值 |
GC日志打印参数参考
gc日志打印参数 |
|
参考资料:
《深入了解jvm虚拟机》
https://blog.csdn.net/qq_31156277/article/details/79962445
https://www.cnblogs.com/yang-hao/p/5936059.html
https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://www.cnblogs.com/yunxitalk/p/8987318.html
https://blog.csdn.net/qq_31156277/article/details/79951819
https://blog.csdn.net/hutongling/article/details/69908443