作者:Eric Fu\
链接:https://ericfu.me/g1-garbage-collector/
在过来很长一段时间内,HotSpot JVM 的首选垃圾收集器都是 ParNew + CMS 组合。直到 JDK7 中 Hotspot 团队首次颁布了 G1(Garbage-First),并在 JDK9 中用 G1 作为默认的垃圾收集器。咱们团队最近也将用了很多年的 CMS 换成了 G1 垃圾收集器。
本文次要从 G1 的论文 Garbage-First Garbage Collection 登程,联合其余较新的白皮书等,解说 G1 垃圾收集器的工作原理。
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.63.6386&rep=rep1&type=pdf
Motivation
对于为什么要从新设计一个 G1 垃圾收集器,论文中给出的理由相当简略:现有的垃圾收集器无奈满足 软实时(Soft Real-time) 个性:即让 GC 进展能大抵管制在某个阈值以内,然而又不用像实时零碎那样十分严格。这也是很多业务零碎都有的诉求。
在过来的 JVM 设计中,如下图所示,堆内存被宰割成几个区域 —— Eden、Survivor、Old 的大小都是事后划分好的。对于总内存 64GB 的机器,可能 Old 区大小就有 32GB,即应用并行的形式收集一次依然须要数秒。近十年,随着内存越来越大,这一问题也变得更为严重。
为了达到软实时的指标,同时也是为了更好地应答大内存,G1 将中不再应用上述的内存布局。
根本数据结构
首先,咱们介绍 G1 种最外围的两个概念:Region 和 Remember Set。
Heap Regions
如下图所示,G1 垃圾收集器将堆内存空间分成等分的 Regions,物理上不肯定间断,逻辑上形成间断的堆地址空间。各个 Mutator 线程(即用户利用的线程)领有各自的 Thread-Local Allocation Buffer (TLAB),用于升高各个线程分配内存的抵触。
要特地留神的是,巨型对象(Humongous Object),即大小超过 3/4 的 Region 大小的对象会作非凡解决,调配到由一个或多个间断 Region 形成的区域。巨型对象会引起其余一些问题,不过这些曾经超出了本文的领域,总之记得尽量别用就好了。
默认配置下,在满足 Region Size 是 2 的整数幂的前提下,G1 将总内存尽量划分成大概 2048 个 Region。
Remember Set (RSet)
为什么要把堆空间分成 Region 呢?其次要目标是让各个 Region 绝对独立,能够别离进行 GC,而不是一次性地把所有垃圾收集掉。咱们晓得古代 GC 算法都是基于可达性标记,而这个过程必须遍历所有 Live Objects 能力实现。那问题来了,如果为了收集一个 Region 的垃圾,却残缺的遍历所有 Live Objects,这也太节约了!
所以,咱们须要一个机制来让各个 Region 能独立地进行垃圾收集,这也就是 Remember Set 存在的意义。每个 Region 会有一个对应的 Remember Set,它记录了 哪些内存区域中存在对以后 Region 中对象的援用。(all locations that might contain pointers to (live) objects within the region)
留神 Remember Set 不是间接记录对象地址,而是记录了那些对象所在的 Card 编号。所谓 Card 就是示意一小块(512 bytes)的内存空间,这外面很可能存在不止一个对象。然而这曾经足够了:当咱们须要确定以后 Region 有哪些对象存在内部援用时(这些对象是可达的,不能被回收),只有扫描一下这块 Card 中的所有对象即可,这比扫描所有 live objects 要容易的多。
实现上,Remember Set 的实现就是一个 Card 的 Hash Set,并且为每个 GC 线程都有一个本地的 Hash Set,最初的 Remember Set 实际上是这些 Hash Set 的并集。当 Card 数量特地多的时候会进化到 Region 粒度,这时候就要扫描更多的区域来寻找援用,工夫换空间。
Remember Set 的保护
保护下面所说的 Remember Set 势必须要记录对象的援用,通常的做法是在 set 一个援用的时候插入一段代码,这称为 Write Barrier。为了尽可能升高对 Mutator 线程的影响,Write Barrier 的代码该当尽可能简化。G1 的 Write Barrier 实际上只是一个“告诉”:将以后 set 援用的事件放到 Remember Set Log 队列中,交给后盾专门的 GC 线程解决。
Write Barrier 具体实现如下。当产生 X.f = Y
时,假如 rX
为 X 对象的地址,rY
为 Y 对象的地址,则 Write 的同时还会执行以下逻辑:
t = (rX XOR rY) >> LogOfRegionSize // 对 X, Y 地址右移失去 Region 编号,并将二者做个 XOR
if (rY == NULL ? 0 : t) // 疏忽两种状况:X.f 被赋值为 NULL,或 X 和 Y 位于同一个 Region 内
rs_enqueue(rX) // 如果 Card(X) 还不是 dirty 的,将 X 的地址放进 Log,并把该 card 置为 dirty
这里 Dirty Bit 的作用是去除反复的 Cards,思考到一个 Cards 内常常产生密集的援用赋值(比方对象初始化),去重一下能大幅缩小冗余。
最初,后盾的 GC 线程则负责从 Remember Set Log 一直取出这些援用赋值产生的 Cards,扫描下面所有的对象,而后更新相应 Region 的 Remember Set。在并发标记产生之前,G1 会确保 Remember Set Log 中的记录都解决完,从而保障并发标记算法肯定能拿到最新的、正确的 Remember Set。
极其状况下,如果后盾的 GC 过程追不上 Mutator 过程写入的速度,这时候 Mutator 线程会进化到本人解决更新,造成反压机制。
Generational Garbage-First
G1 名字来自于 Garbage-First 这个理念,即,以收集到尽可能多的垃圾为第一指标。每次收集时 G1 会选出垃圾最多的几个 Region,进行一次 Stop-the-world 的收集过程。
乏味的是,另一方面 G1 又是一个 Generational(分代)的垃圾收集器,它会从逻辑上将 Region 分成 Young、Old 等不同的 Generation,而后针对它们各自特点利用不同的策略。
G1 论文中提到它有一个 Pure Garbage-First 的模式,但在当初的材料中曾经很难看到它的踪影,我猜想理论应用中 Generational 模式要成果好的多。以下咱们也会只探讨 Generational 模式的工作形式。
经典的内存布局中,各代的内存区域是齐全离开的,而 G1 中的 Generation 只是 Region 的一个动静标记,下图是一个标记了 Generation 的例子。各个 Region 的 Generation 是随着 GC 的进行而一直变动的,甚至各个代有多少 Region 这个比例也是随时调整的。
Evacuation
为了不便读者了解 G1 收集的过程,咱们先看下 Evacuation 的过程,之后再看如何做 Marking。
Generational 模式下 G1 的垃圾收集分为两种:Young GC 和 Mixed GC。Young GC 只会波及到 Young Regions,它将 Eden Region 中存活的对象挪动到一个或多个新调配的 Survivor Region,之前的 Eden Region 就被偿还到 Free list,供当前的新对象调配应用。
当区域中对象的 Survive 次数超过阈值(TenuringThreshold
)时,Survivor Regions 的对象被挪动到 Old Regions;否则和 Eden 的对象一样,持续留在 Survivor Regions 里。
屡次 Young GC 之后,Old Regions 缓缓累积,直到达到阈值(InitiatingHeapOccupancyPercent
,简称 IHOP),咱们不得不对 Old Regions 做收集。这个阈值在 G1 中是依据用户设定的 GC 进展工夫动静调整的,也能够人为干涉。
对 Old Regions 的收集会同时波及若干个 Young 和 Old Regions,因而被称为 Mixed GC。Mixed GC 很多中央都和 Young GC 相似,不同之处是:它还会抉择若干最有后劲的 Old Regions(收集垃圾的效率最高的 Regions),这些选出来要被 Evacuate 的 Region 称为本次的 Collection Set (CSet)。
Mixed GC 的重要性显而易见:Old Regions 的垃圾就是在这个阶段被收集掉的,也正是因为这样,Mixed GC 是工作量最为沉重的一个环节,如果不加以控制,就会像 CMS 一样产生长时间的 Full GC 进展。这时候 Region 的设计就施展出优越性了:只有把每次的 Collection Set 规模管制在肯定范畴,就能把每次收集的进展工夫软性地管制在 MaxGCPauseMillis
以内。起初这个管制可能不太精准,随着 JVM 的运行估算会越来越精确。
那来不及收集的那些 Region 呢?多来几次就能够了。所以你在 GC 日志中会看到 continue mixed GCs
的字样,代表分批进行的各次收集。这个过程会多次重复,直到垃圾的百分比降到 G1HeapWastePercent
以内,或者达到 G1MixedGCCountTarget
下限。
对于 Young Regions,咱们对它有以下非凡优化:
- Evacuation 的时候,Young Regions 肯定会被放到待收集的 Regions 汇合(Collection Set)中,起因很简略,绝大多数对象寿命都很短,在 Young Regions 做收集往往绝大部分都是垃圾。
- 因为 Young Regions 肯定会被收集,咱们取得了一个可观的收益:Remember Set 的保护工作不须要思考 Young 内的援用批改(换句话说 RSet 只关怀 old-to-young 和 old-to-old 的援用),当 Young Region 上产生 Evacuation 时咱们再去扫描并构建出它的 RSet 即可。
Concurrent Marking
在 Evacuation 之前,咱们要通过并发标记来确定哪些对象是垃圾、哪些还活着。G1 中的 Concurrent Marking 是以 Region 为单位的,为了保障后果的正确性,这里用到了 Snapshot-at-the-beginning(SATB)算法。
SATB 算法顾名思义是对 Marking 开始时的一个(逻辑上的)Snapshot 进行标记。为什么要用 Snapshot 呢?上面就是一个间接标记导致问题的例子:对象 X 因为没有被标记到而被标记为垃圾,导致 B 援用生效。
SATB 算法为了解决这一问题,在批改援用 X.f = B
之前插入了一个 Write Barrier,记录下被覆写之前的援用地址。这些地址最终也会被 Marking 线程解决,从而确保了所有在 Marking 开始时的援用肯定会被标记到。这个 Write Barrier 伪代码如下:
t = the previous referenced address // 记录本来的援用地址
if (t has been marked && t != NULL) // 如果地址 t 还没来的及标记,且 t 不为 NULL
satb_enqueue(t) // 放到 SATB 的待处理队列中,之后会去扫描这个援用
通过以上措施,SATB 确保 Marking 开始时存活的对象肯定会被标记到。
标记的过程和 CMS 中是相似的,能够看作一个优化版的 DFS:记以后曾经标记到的 offset 为 cur
,随着标记的进行 cur 一直向后推动。每当拜访到地址 < cur 的对象,就对它做深度扫描,递归标记所有利用;反之,对于地址 > cur 的对象,只标记不扫描,等到 cur 推动到那边的时候再去做扫描。
上图中,假如以后 cur 指向对象 c,c 有两个援用:a 和 e,其中 a 的地址小于 cur,因此做了扫描;而 e 则仅仅是标记。扫描 a 的过程中又发现了对象 b,b 同样被标记并持续扫描。然而 b 援用的 d 在 cur 之后,所以 d 仅仅是被标记,不再持续扫描。
最初一个问题是:如何解决 Concurrent Marking 中新产生的对象?因为 SATB 算法只保障能标记到开始时 snapshot 的对象,对于新呈现的那些 对象,咱们能够简略地认为它们全都是存活的,毕竟数量不是很多。
最初,关注公众号 Java 技术栈,在后盾回复:JVM46,能够获取一份 46 页的 JVM 调优教程。
参考资料:
- Detlefs, David, et al Garbage-First Garbage Collection. Proceedings of the 4th international symposium on Memory management. ACM, 2004.
- Printezis, Tony, and David Detlefs. A Generational Mostly-Concurrent Garbage Collector. Vol. 36. No. 1. ACM, 2000.
- https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf
- https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector1
- https://www.redhat.com/en/blog/collecting-and-reading-g1-garbage-collector-logs-part-2
- https://www.slideshare.net/MonicaBeckwith/con5497
- https://blog.csdn.net/coderlius/article/details/79272773
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)
2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!
3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!