JVM-GC(2)
垃圾收集器
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器运行的6个阶段:
- 初始标记
- 并发标记
- 并发预清理
- 最终标记
- 并发清除
- 并发重置
其中,第1、4阶段仍然需要STW,第2、3、5、6阶段都可以与用户线程同时工作。初始标记仅仅知识标记一下GC Roots能直接关联到的对象,速度很快;最终标记则是为了修正并发标记阶段时产生变动的标记,速度没有初始标记快但还是比并发标记快得多,所以第1、4阶段虽然需要STW,但都是极其短暂的停顿。
(此处应有图)
可以看出,CMS的优点:
- 并发收集
- 低停顿
但CMS至少还有三个明显缺点:
- CMS收集器对处理器资源非常敏感。
在进行并发收集时,虽然不会导致用户线程停顿,但会因为与用户线程一起运行而占用一部分资源,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,所以当处理器核心数在4个或以上,并发回收时垃圾收集线程只占用不少于25%的处理器运算资源,并随着处理器核心数的增加而下降;而当处理器核心数不足4个时,CMS对用户程序的影响就可能变大。
- 内存碎片化
和其他基于标记-清除算法实现的收集器一样,CMS同样会有内存碎片化的问题。碎片过多时,会给分配大对象带来麻烦,往往在老年代还有很多剩余空间时但由于无法找到足够大的连续空间来分配大对象而导致提前Full GC。
为了解决这个问题,可以使用-XX:UseCMSCompactAtFullCollection,在Full GC时开启内存碎片的合并整理。由于整理过程是无法并发的,导致Full GC的时间会变得更长。但这个参数在JDK9后被废弃,很好理解啦,因为JDK9有G1了嘛。
- Concurrent Mode Failure
一般会有两种情况触发Concurrent Mode Failure:
- 对象晋升失败
- 浮动垃圾
浮动垃圾
浮动垃圾是指,在初始标记是被标记为可达对象,但在最终标记后被改为不可达对象,但该对象对于收集器来说还是可达的这部分对象。与标记底层原理有关(三色标记)。如果解决?与写屏障有关。
由于CMS无法处理浮动垃圾,有可能引发Concurrent Mode Failure进而导致一次Full GC的发生。
对象晋升失败
该情况会发生在Minor GC时,当对象达到晋升条件或者Survivor space放不下对象时,而老年代又因为开启不及时导致没有多余的空间存放这些晋升的对象,这时就会触发Concurrent Mode Failure,CMS会退化成Serial Old收集器的行为:STW、使用单线程收集、整理老年代。
怎么解决?
- 使用参数-XX:CMSInitiatingOccu-pancyFraction来调整CMS会在内存空间占用率达多少的时候开启垃圾收集。
- 使用参数-XX:+UseCMSCompactAtFullCollection整理内存碎片。
CMS调优
要对CMS进行调优,首先要从影响CMS性能的问题入手:
- 晋升阈值太小
- Survivor空间过小
- Eden区过小,导致晋升过快
- 存在大对象
以上4中情况都会导致老年代垃圾产生过快,所以要对CMS调优就需要对这4方面做调整。
G1收集器
根据G1的论文 Garbage-First Garbage Collection,设计G1的目的是为了满足软实时特性(Soft Real-time):即让 GC 停顿能大致控制在某个阈值以内,但是又不必像实时系统那样非常严格。这也是很多业务系统都有的诉求。所以基于软实时特性,G1的一大特性是:G1在停顿时间上添加了可预测机制,用户可以指定期望停顿时间。
与CMS一样,G1的垃圾收集线程能和用户线程并行,除此之外还有一些更优越的特性:
- 在兼顾低延迟的同时,不会牺牲吞吐量
- 不会产生大量内存碎片
- 整理空闲时间更快
- 在大堆上的表现更好
- 停顿时间可控
内存布局
与其他分代收集器不一样,将堆分成几块区域:eden
,old
,metaspace
;G1将堆空间分成大小相等的Regions,各个Region在物理上不连续,在逻辑上的是连续的地址空间。默认配置下,G1会尽量将内存划分成2048个regions。
虽然G1的内存布局和其他分代收集器不一样,但它还是一个分代收集器。Region在逻辑上可分为young region
,old region
,survivor region
,Humongous region
这4种不同的Generation。Generation可以理解为对Region的一种动态标记,动态标记会随着GC的进行不断的变化。
![](https://img.haomeiwen.com/i9620895/4e9517c118dbb0f2.png)
![](https://img.haomeiwen.com/i9620895/dcb5c1a223be506d.png)
收集过程
G1GC主要有两个功能:
- 全局并发标记 Global Concurrent Marking
- 转移 Evacuation
并发标记基本能和用户线程并发执行,会针对region内所有的存活对象进行标记。转移负责释放堆中死亡对象所占的内存空间。转移的过程中同时会进行整理,因此G1GC中的region不会产生内存碎片。这两部分可以独立进行。
转移 Evacuation
G1GC有两种模式:
- young gc
- mixed gc
young gc和mixed gc都会STW。在young gc和mixed gc阶段,被选中收集的regions会被放入Collection Set(CSet)中。
young gc
young gc针对young region=eden region+survivor region的收集:
- 将所有young regions添加到CSet中
- 选定CSet后的evacuation与ParallelScavenge算法类似,将eden regions中存活的对象转移到一个或多个新分配的survivor regions,之前的eden regions会被放入CSet中,就会被归还到free list,供以后新对象分配使用。
![](https://img.haomeiwen.com/i9620895/28882084509ecf85.png)
当survivor regions中的对象存活次数超过阈值TenuringThreshold时,survivor regions的对象被转移到old regions中,否则和eden regions的对象一样,继续留在survivor regions中。
![](https://img.haomeiwen.com/i9620895/db08e4250b152b7b.png)
mixed gc
多次young gc之后,old regions中的对象慢慢积累,直到到达阈值InitiatingHeapOccupancyPercent(IHOP,默认值45%),G1不得不对old regions做收集。这个阈值IHOP在G1中是根据用户设定的GC停顿时间动态调整的,也可以人为设置
对old regions的收集会涉及若干个young regions和old regions,因此被称为mixed gc。mixed gc大体与young gc类似,也是先将所有young regions添加进CSet,不同的地方在于:mixed gc会根据global concurrent marking统计结果,选择垃圾收集收益最高的几个old regions添加进CSet。
![](https://img.haomeiwen.com/i9620895/179eb22a04f4d03f.png)
mixed gc不是full gc,因为mixed gc只会收集部分old regions。但G1也会出现与CMS一样的极端情况:当在mixed gc期间出现所有老年代被占用完,无法为晋升的对象或者大对象分配的情况,这时G1会采用Serial old收集器来收集,等同于经历Full GC。
总结
- 可以看到young region总是在CSet内,所以G1从不维护从young region出发引用的RSet的更新。
- old region的回收完全依赖mixed gc。
全局并发标记 Global Concurrent Marking
全局并发标记的作用是标记old region,提供统计结果供mixed gc使用,帮助选取收益高的old region进行收集。G1的工作流程:
- evacuation阶段,在young gc和mixed gc中切换
- 定期做global concurrent marking。G1所有有关concurrent的操作都在Global Concurent Marking阶段进行。
Global Concurrent Marking是基于SATB算法实现的一种并发标记。SATB全称是Snapshot-At-The-Beginning,在SATB并发标记的过程中,有可能会造成白色对象漏标的情况,这时候需要添加写屏障来保证。
根据三色标记法,为了解决在并发标记过程中,存活对象漏标的情况,GC HandBook把对象分成三种颜色:
黑色:自身以及可达对象都已经被标记
灰色:自身被标记,可达对象还未标记
白色:还未被标记所以,漏标的情况只会发生在白色对象中,且满足以下任意一个条件:
- 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象
- 并发标记时,应用线程删除所有灰色对象到该白色对象的引用
对于第一种情况,利用post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍
对于第二种情况,利用pre-write barrier,将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍
将漏标的两个条件打破其中一个就能保证不漏标了,CMS 和 G1正好是两种不同的策略,CMS 是 post-write barrier,记录新对象引用,随后 remark 重新扫描(rescan)即可;G1则是 pre-write barrier ,记录删除的引用,并且假定快照之后新增的引用对象都是活对象。
重要控制参数
什么时候触发global concurrent marking?
当整个堆占用超过-XX:InitiatingHeapOccupancyPercent(默认值45),就会触发Global Concurrent Marking。当该值设置的比较小时,意味着提前global concurrent marking,也意味着mixed gc会提前开始,这样有利于防止young gen晋升old gen失败而触发Full GC。
当-XX:InitiatingHeapOccupancyPercent设置比较小时,GC日志中会频繁出现:concurrent-root-region-scan-start,说明global concurrent marking工作开始。
什么时候触发Mixed GC?
在全局并发标记结束后能够统计出所有可被回收的垃圾占Heap的比例值,如果超过-XX:G1HeapWastePercent(默认值5%),就会触发多轮mixed gc。
哪些参数影响old region 进入CSet?
-XX:G1MixedGCLiveThresholdPercent=65:设置old region中存活对象百分比,只有在此百分比以下的region才能进入Cset,默认值65%。
-XX:G1OldCSetRegionThresholdPercent=10:设置在一次mixed gc中old regions大小总和占堆比例上限,默认值10%,即一次mixed gc中回收的old regions大小总和不能超过堆大小的10%。
辅助标记 Remembered Sets和Card Table
- RSet:Remembered Sets,用来记录其他region指向本region中对象的所有引用,每个region维护自家的RSet。
- Card:region被划分为多个card,可类比内存中page的概念。
上面提到,G1从不维护从young region出发引用的RSet的更新,所以这两个数据结构维护的是从old region出发到young region的引用。
下图展示的是RSet和card的关系:每个region被划分为多个card,其中绿色部分的Card表示自家Card中有对象引用了别家Card中的对象,这种引用关系用蓝色实线表示,而region中自家的RSet记录的引用了自家card中对象的别家card信息,所以自家Rset记录的是别家的东西。
![](https://img.haomeiwen.com/i9620895/f0c12830746ea039.png)
每个Region初始化时,会初始化一个Rset,RSet其实是一个HashTable,Key是Region的起始地址,Value是Card Table (字节数组),字节数组下标表示Card的空间地址,当该地址空间被引用的时候会被标记为dirty_card。所以RSet需要记录的是XX region的XX card,指的是别家XX region的XXcard引用了自家。
利用Rset,标记时就可以得知自家region中有多少被引用的存活对象,如果没有Rset,那么标记时就得扫描整个region。
RSet有什么风险?
通过对RSet实现过程的研究,我们得知应用线程只负责把更新字段所在的Card插入到dirty card queue中,然后由后台线程refinement threads负责RSet的更新操作,如果应用线程插入速度过快,refinement threads来不及处理,那么应用线程将接管RSet更新的任务,这是必须要避免的。
refinement threads线程数量可以通过-XX:G1ConcRefinementThreads或-XX:ParallelGCThreads参数设置
网友评论