G1
适用于多核、大内存的机器上。大多数情况下可以指定GC暂停时间,并且可以保持较高的吞吐量。
特点
- 并发标记,并发收集
- 压缩空间,不会延长GC暂停时间。(复制算法)
- 更容易预测GC暂停时间。
- 适用于吞吐量不高的场景。
与CMS相比
- 不会产生很多内存碎片。
- 可以调节GC的暂停时间。
基本概念
Region
传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:
而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:
CSET: collection set
它记录了GC要收集的Region集合。当CSET中Region的垃圾被回收之后,在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。
CardTable
Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。
YGC的时候,从根上面去查找的时候。A(Y) > B(O) > C(Y),需要扫描整个old区,效率很低。这时候JVM设计了CardTable,如果一个O区Card中有对象指向Y区,就标记这个Card为dirty。remark时候,就只会扫描dirty的对象。cardtable就是记录o区中,是否是dirty对象的一个table。
RSET:Remembered Set
RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。
G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。
BitMap
参考 https://zhuanlan.zhihu.com/p/71058481
用来标记对象的存活状态。在Mix回收的时候,会根据Bigmap来计算垃圾的占比,来决定要不要把这个Region放入CSet中。
Region的指针
针对 Region 本身,需要重点理解 Region 中的五个指针
⁃ Bottom 指向 Region 起点;
⁃ Top 当前Region 分配对象的游标,Top 永远指向当前Region 最新分配的对象;
⁃ PrevTAMS 和 NextTAMS 分别标记前后两次并发标记周期开始时 Top 指针的位置 (TAMS - top at mark start);
⁃ End 表示 Region 终点。
-
[Bottom,PrevTAMS]-> 这部分的存活信息会在previous marking bitmap体现;(上次并发标记过程中,[PrevTAMS, NextTAMS] 新分配的对象。)
-
[PrevTAMS, NextTAMS]-> 全量标记过程中,新分配的对象,隐式存活;
-
[NextTAMS, Top]-> 全量标记完成后,再次全量标记之前,新分配的对象,隐式存活;
三色标记算法
-
白色节点:尚未被标记的对象;
-
黑色节点:已经被标记,且其引用关系已经被处理;
-
灰色节点:已经被标记,但引用关系尚未被处理;
标记流程如下:
参考 https://hllvm-group.iteye.com/group/topic/44381
-
初始标记(initial marking):
暂停阶段。扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用young GC的暂停,因而没有额外的、单独的暂停阶段。
-
并发标记(concurrent marking):
并发阶段。不断从扫描栈取出引用递归扫描整个堆里的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。
-
最终标记(final marking,在实现中也叫remarking)
暂停阶段。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。
注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。
-
清理(cleanup)
暂停阶段。清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,不过不是在堆上sweep实际对象,而是在marking bitmap里统计每个region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。
漏标
发生漏标的充分必要条件:
-
Mutator赋予一个黑对象该白对象的引用。
-
Mutator删除了所有从灰对象到该白对象的直接或者间接引用。
incremental-update 增量更新(CMS使用方式)
当A指向D的时候,将A标记为灰色。remark会重新扫描A。
SATB snapshot at the begining(G1使用方式)
当B不再指向D的时候,把这个消失的引用推到GC的堆栈。保证D还可以被GC扫描到。(只需要查看Rset区域就可以了,记录了自己被指向的引用)
GC回收
- Young GC:选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。
- Mixed GC:选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。在用户指定的开销目标范围内尽可能选择收益高的old gen region。
可以看到young gen region总是在CSet内。因此分代式G1不维护从young gen region出发的引用涉及的RSet更新。(不理解)
分代式G1的正常工作流程就是在young GC与mixed GC之间视情况切换,背后定期做做全局并发标记。
在正常工作流程中没有full GC的概念,old gen的收集全靠mixed GC来完成。
如果mixed GC实在无法跟上程序分配内存的速度,导致old gen填满无法继续进行mixed GC,就会切换到G1之外的serial old GC来收集整个GC heap(注意,包括young、old、perm)。这才是真正的full GC。
常用参数
一般参数
-
-XX:G1HeapRegionSize=n 设置Region大小,并非最终值
-
-XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms,预期值。
-
-XX:G1NewSizePercent 新生代所在空间最小值,默认值5%
-
-XX:ParallelGCThreads 在清理垃圾的时候,并行GC线程数
并发标记参数
-
-XX:ConcGCThreads=n并 发标记阶段,并行执行的线程数
-
-XX:InitiatingHeapOccupancyPercent设置触发标记周期的 Java 堆占用率阈值,默认值是45%。
- 这里的百分比是针对整个堆大小的百分比,而CMS中的CMSInitiatingOccupancyFraction命令选型是针对老年代的百分比。
- 这里的java堆占比指的是内存占比大小。不是垃圾的占比。
MixGc相关参数
-
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数量。
疑点解惑(个人理解)
为什么不直接根据RSET扫描,还要从根查
因为无法确定,指向自己的那个对象,是否还存在。
白色对象如果直接指向黑色对象为什么不会发生漏标:
- 首先明确一点,新建的对象是首先在survivor区的,默认就会存活,因此不会有白色标记。(该标记会存放到PTAMS-NTAMS阶段)
- 其次,在黑色对象扫描完成的时候,它的指向的对象已经全是灰色了。因此白色对象一定是直接或间接挂载灰色对象上面的。这时候如果有白色对象指向黑色,一定会先断掉与灰色的连接。
BitMap的意义
可以记录新创建的对象的分配情况,根据Bitmap的记录来判断每个Region的垃圾占比,从而进行选择性回收。
全量标记何时触发:
当内存占比达到一定条件的时候,可以通过参数指定。
MixGc何时触发
在标记完成之后,发现垃圾占比达到一定比例。
回收什么样的Region
垃圾占比达到该Region的一定比例的时候。
如何实现减少碎片化:
CSET中Region的垃圾被回收之后,在CSet中存活的数据会在GC过程中被移动到另一个可用Region
Remark阶段,G1需要扫描survivor区吗?
不需要,直接默认存活。
CMS和G1的优化点在哪?
-
Remark阶段更快
CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。
G1只需要扫描SATB buffer,找到Region的Rset,然后去找引用自己的对象。如果那个对象存在,则表示自己存活。
-
减少碎片化
MixGc阶段,使用copy清除,可以减少碎片化。
说明
本文章是自己的一些简单学习,一些细节点理解并不透彻。欢迎留言讨论,互相学习。