G1

适用于多核、大内存的机器上。大多数情况下可以指定GC暂停时间,并且可以保持较高的吞吐量。

特点

  • 并发标记,并发收集
  • 压缩空间,不会延长GC暂停时间。(复制算法)
  • 更容易预测GC暂停时间。
  • 适用于吞吐量不高的场景。

与CMS相比

  1. 不会产生很多内存碎片。
  2. 可以调节GC的暂停时间。

基本概念

Region

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:

G1垃圾回收器的整理学习

而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:

G1垃圾回收器的整理学习

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。

G1垃圾回收器的整理学习

上图中有三个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 终点。

G1垃圾回收器的整理学习

  • [Bottom,PrevTAMS]-> 这部分的存活信息会在previous marking bitmap体现;(上次并发标记过程中,[PrevTAMS, NextTAMS] 新分配的对象。)

  • [PrevTAMS, NextTAMS]-> 全量标记过程中,新分配的对象,隐式存活;

  • [NextTAMS, Top]-> 全量标记完成后,再次全量标记之前,新分配的对象,隐式存活;

三色标记算法

  • 白色节点:尚未被标记的对象;

  • 黑色节点:已经被标记,且其引用关系已经被处理;

  • 灰色节点:已经被标记,但引用关系尚未被处理;

标记流程如下:

参考 https://hllvm-group.iteye.com/group/topic/44381

  1. 初始标记(initial marking):

    暂停阶段。扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用young GC的暂停,因而没有额外的、单独的暂停阶段。

  2. 并发标记(concurrent marking):

    并发阶段。不断从扫描栈取出引用递归扫描整个堆里的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。

  3. 最终标记(final marking,在实现中也叫remarking)

    暂停阶段。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。

    注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。

  4. 清理(cleanup)

    暂停阶段。清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,不过不是在堆上sweep实际对象,而是在marking bitmap里统计每个region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。

漏标

G1垃圾回收器的整理学习

发生漏标的充分必要条件:

  • 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%。

    1. 这里的百分比是针对整个堆大小的百分比,而CMS中的CMSInitiatingOccupancyFraction命令选型是针对老年代的百分比。
    2. 这里的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扫描,还要从根查

因为无法确定,指向自己的那个对象,是否还存在。

白色对象如果直接指向黑色对象为什么不会发生漏标:

  1. 首先明确一点,新建的对象是首先在survivor区的,默认就会存活,因此不会有白色标记。(该标记会存放到PTAMS-NTAMS阶段)
  2. 其次,在黑色对象扫描完成的时候,它的指向的对象已经全是灰色了。因此白色对象一定是直接或间接挂载灰色对象上面的。这时候如果有白色对象指向黑色,一定会先断掉与灰色的连接。

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清除,可以减少碎片化。

说明

本文章是自己的一些简单学习,一些细节点理解并不透彻。欢迎留言讨论,互相学习。

相关文章:

  • 2021-04-22
  • 2021-11-18
  • 2021-11-02
  • 2021-09-19
  • 2021-07-05
  • 2021-08-18
  • 2022-01-23
猜你喜欢
  • 2021-05-06
  • 2021-07-23
  • 2021-08-29
  • 2021-08-01
相关资源
相似解决方案