【转载】深入分析 G1 垃圾收集器实现原理

原文链接:https://juejin.cn/post/7050324680875442183

深入分析 G1 垃圾收集器实现原理

1 与垃圾收集器有关的算法

​ 在分析 G1 前先简单回顾一下与垃圾收集器相关的算法。通常所谓的垃圾收集器更多地是指跟踪垃圾收集器(Tracing Garbage Collection),而不是引用计数(Reference Counting )垃圾收集器。跟踪垃圾收集器采用可达性分析方法确定哪些对象要被回收,通常会选取一些对象作为 GC Roots,如果对象能直接或间接地被 GC Roots 中的对象引用,则认为该对象可达(存活对象)不能被回收,否则该对象不可达(垃圾对象)要被回收。

1.1 三色标记算法

​ 在确定内存中哪些对象是垃圾对象时,可以采用最简单的标记算法,即给内存中每个对象一个专门的标记位,被标记则认为是存活对象,否则是垃圾对象,然从 GC Roots 的对象集合开始递归遍历对象图,如果对象图中的对象能被 GC Roots 中的对象直接或简接引用则进行标记。理论上该算法可以确定内存中哪些对象是存活的,哪些对象是垃圾,但整个过程应用程序必须暂停,并且要处理所有的内存区域。

​ 为了解决上面的问题,Dijkstra 等人在On-the-Fly Garbage Collection: An Exercise in Cooperation 一文中提出了三色标记(Tri-color Marking)算法。像 Go、JavaScript 、Java 等语言在内存回收上都采用了三色标记算法的变种。

​ **三色标记算法会创建白色、灰色、黑色三个集合,三个集合内分别只存储白色对象、灰色对象、黑色对象。白色对象,代表尚未开始标记的对象或已完成标记并确认为垃圾的对象;灰色集合,代表还在标记中的对象,即遍历对象图时已遍历到自己,但还未完成自己引用 对象的遍历;黑色对象,代表已完成标记并确认为存活的对象(正常情况下,对象标记的颜色变化只能白色变成灰色,灰色变成黑色)。起初黑色集合通常为空,灰色集合内为 GC Roots 直接引用的对象,其他对象均在白色集合内。一个对象任一时刻只能在白色、灰色、黑色三个集合中的某一个。**通常三色标记算法的处理流程如下:

1、起初除 GC Roots 外的其他对象全白色集合,将 GC Roots 直接引用的对象从白色集合内移到灰色集合。

2、从灰色集合取出一个灰色对象,依次处理该对象引用的对象。若其未引用任何对象,则直接将其移入黑色集合中;若其引用的对象在白色集合中则将其移入灰色集合,否则直接不处理,当该灰色对象引用的对象全处理完后,再将其移入黑色集合中。

3、重复第 2 步的流程直到灰色集合为空。

4、上面的步骤处理完后,GC Roots 与黑色集合内的对象为存活对象,而白色集合内的对象为垃圾对象,最后要做的就是将白色集合内的垃圾对象清理。

​ 下图展示了除 GC Roots 另外有 8 个对象时,三色标记算法的处理流程。

  • 起初除了 GC Roots 内的对象外,其他对象全在右则的白色集合中。

  • 将 GC Roots 直接引用的对象从白色集合内移到灰色集合后,此时 A 对象与 F 对象已从白色集移到灰色集合。

  • 处理完灰色集合中的 A 对象引用的 B 对象后,此时 B 对象已从白色集移到灰色集合。

  • 处理完灰色集合中的 A 对象引用的 C 对象与 D 对象后。此时 A 对象已从灰色集合移到黑色集合,C 对象与 D 对象已从白色集移到灰色集合。

  • 处理灰色集合中剩余的 B 对象、C 对象、D 对象与 F 对象后,B 对象、C 对象、D 对象与 F 对象象已从灰色集移到黑色集合。

  • 经历过上面的处理后灰色集合已为空,三色标记法标记阶段结束到达清理阶段,白色集合中的 E 对象、G 对象与 H 对象被清理,最后结果如下图。

1.2 三色标记算法的不足

​ 如果应用程序线程与三色标记算法的 GC 线程一起运行,则可能出现对象错标与漏标。所谓的对象错标是指原为是垃圾的对象被标记为黑色认为是存活的,这种情况的出现并不会引起应用程序的错误,只是会将垃圾收集的时间拖延到下一次垃圾回收。而对象漏标,则是原本要标记为黑色的对象,被遗漏了,没有被标记,最终导致该对象在白色集合中被垃圾回收集给回收掉;这种情况的一旦发生应用程序将出现未知的异常,这个异常可能是无关紧要的也可能是致命的。

​ 以上面的例子来看看漏标是怎么发生的。

假设 GC 线程准备下一步标记工作前,对象的标记状态如上图。此时 GC 线程下一步将处理灰色集合中的 F 对象,由于 F 对象未引用任何对象其将直接移动到黑色集合中,整体个灰色集合为空标记结束。可是如果在 GC 线程还未完成 F 对象从灰色集合转移到黑色集合的操作时,应用线程正好增加了 F 对象对 G 对象的引用呢?

​ 由于 F 对象已结束标记工作(实际 GC 线程已认为 F 对象是黑色的),F 对象最终还是会从灰色集合成功地转移到黑色集合。而 GC 线程将无法感知应用程序新增加的 F 对象到 G 对象的引用,最终导致 G 对象的漏标。实际上产生漏标一定会满足下面两种情况的一种。

1、GC 线程标记的过程中,应用线程增加黑色对象到白色对象的引用

2、GC 线程标记的过程中,应用线程删除了灰色对象到白色对象的引用

上面的漏标示例实际是第一种情况,论文Uniprocessor Garbage Collection Techniques 的 3.2.1 Incremental approache 小节将处理漏标时关注的点不同将 GC 分为 Snapshot-at-beginning collectors 与 Incremental update collectors。Snapshot-at-beginning collectors 关注于处理第一种情况,而 Incremental update collectors 关注于处理第二种情况,G1 属于 Snapshot-at-beginning collectors,而 CMS 属于 Incremental update collectors。G1 并发标记过程关注处理应用线程增加黑色对象到白色对象的引用,即当黑色对象新引用了白色对象时,便将这个黑色对象重新设置为灰(技术实现上采用 pre-write barrier) ;而 CMS 发标记过程关注处理线程删除了灰色对象到白色对象的引用,即当灰色对象删除了白色对象的引用时,便将这个白色对象直接置灰(技术实现上采用 post-write barrier)。

​ 那具体采用何种技术手段处理上面的两种情况呢?其实也很简单,就是想办法让 GC 线程感知对象引用的变化,即所谓的写屏障(write barrier)。这里的所说的写屏障并不是硬件层面的写屏障,而是软件层的写屏障,其实质可理解为在引用赋值这个写操作前加一个切面,根据切点加入时机不同又可分为 pre-write barrier 与 post-write barrier,下面是 G1 中采用的写屏障的伪代码实现(来源于[HotSpot VM] 请教 G1 算法的原理 )。

java

复制代码

void oop_field_store(oop* field, oop new_value) { pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant *field = new_value; // the actual store post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference }

G1 中利用 pre-write barrier 来保证并发标记过程中要处理的 SATB(snapshot-at-the-beginning)的完整性(G1 SATB 具体如何的实现后面会详细分析),即 GC 线程在跟踪标记开始阶段生成的对象图快照时,应用线程对该对象图快照的修改能通过 pre-write barrier 感知。另一方面 G1 采用 post-write barrier 来维护并发标记过程中应用线程新产生的需要跟踪的跨区间引用(后面分析 G1 的 RSet 时会再补充说明)。

1.3 对象的清除实现方式

​ 当对象标记结束后,便可以清除对象。而在具体实现时可以采用三种方式标记清除、标记复制、标记压缩算法。标记清除最为简单与高效,其直接将那些垃圾对象清除,但这也带来了内存碎片的问题,同时对象分配时也不得不采用空闲空间列表算法而不能采用高效的指针碰撞算法。标记复制算法通常要额外占用 50%的空间,其实现是一直用一半内存存储对象,而另一半内存置空,当回收垃圾时,将已使用空间中仍存活的对象直接复制到置空的那段内存中,然后直接置空之前使用的那半内存。标记压缩算法,兼顾标记清除与标记复制算法的优点,在回收垃圾后会对存活对象进行相应的移动,尽量将碎片化的内存空间进行压缩。

2 分代垃圾收集器

​ 在分析 G1 垃圾收集器之前有必要先简单回顾一下 HotSpot VM 中的其他垃圾收集器。在 G1 出现之前 HotSpot VM 中的其他垃圾收集器都是基于新生代与年老代进行垃圾回收的,这些垃圾回收器集统称为分代垃圾。而 G1 则是兼顾分代与分区的垃圾收集器。

2.1 分代垃圾收集器垃圾收集过程

​ 分代垃圾收集器将 Heap 划分为新生代(Young Generation)与年老代 (Old Generation),在 JDK1.8 之前还有永久代(Permanent Generation)的概念。新生代又被进一步划分为 Eden、From Space 、To Space,其中 From Space 与 To Space 大小相等又称作 Survivor Spaces。

​ Heap 被划分为新生代与年老代是基于弱分代假设的,在 java 应用程序与其他应用程序中都可以观测到弱分代假设的现象。

1、大多数分配的对象不会被长期引用(被认为是存活的)即他们很年轻就死去。

2、老对象很少持有来自新对象的引用。

新生代垃圾回收相对频繁,且利用的算法高效快速,因为年轻代空间通常很小并且可能包含许多不再被引用的对象。而年老代垃圾回收频率则相对较低,但由于年老代占用内存相对更多,通常老代垃圾回收将更加耗时。新生代与年老代分别存储不同年龄的对象,通常刚分配内存的对象被存储在新生代,每经过一次垃圾回收如果对象还存活其年龄将加 1,当经过多轮垃圾回收后如果对象的年龄超过了 MaxTenuringThreshold 值,该对象将晋升到年老代。

​ 由于新生代垃圾回收相对更加频繁,新生代垃圾回收更加关注垃圾回收的时效性,通常会采用复制算法或标记清除算法处理垃圾回收。年老代占用内存相对更大,而垃圾回收频繁较低,年老代垃圾回收更加关注垃圾回收的空间性,即垃圾回收后能否释放更多连续的内存,通常会采用压缩算法处理垃圾回收。

现在来简单看看对象如何在 Eden、Survivor 与 Old Generation 之间进行分配与转移的。

  • 任何新对象都被分配到新生代的 Eden 空间,当 Eden 区域无法容纳新对象时,会触发一次 Young GC。 最开始时两个 Survivor 空间都是空(下图是已经过若干次 GC 的情况)。

  • Young GC 过程中 Eden 空间仍被引用的对象(存活对象)会被复制到第一个 Survivor 空间(S0 Survivor Space)。 而 Eden 空间未被引用的对象将被直接删除。经历过一次 Young GC 后仍存活的对象,其年龄都会增加 1,下图 S0 Survivor Space 中的对象都只经历一次 Young GC,全被标记为 1。

  • 下一次 Young GC 中仍被引用的对象(存活对象)会被复制到之前是空的 Survivor 空间(To survivor space ,实际是之前的 S1 survivor space),Eden 空间未被引用的对象将被直接删除。 之前的 S0 suvivor space 现在称为 Form survivor space,其中依赖被引用的对象,被复现到了之前是空的 Survivor 空间(To survivor space ),Form survivor space 未被引用的对象将被直接删除。仍被引用的对象从 Form survivor space 复制到 To survivor space 后,其对象年龄将加 1,表明该对象又经历了一次 Young GC。

  • 再下一次 Young GC 中,会重复上面相同的过程。 但这时 Survivor space 角色将进行交换,即 From survivor space 变成 To survivor space,To survivor space 变成 From survivor space。这个交换的目的实际就是为了将已使用的 Survivor space 中仍存活的对象复制到被清空的 Survivor space 中。

  • **当经历很多次 Young GC 后新生代中仍存活的对象将会晋升(Promotion)到老年代。下图展示了当 MaxTenuringThreshold 参数为 8 时,仍存活的对象从新生代的 From survivor space 晋升到老年代。 **

  • 随着 Young GC 的不断发生,新生代中仍存活的对象将不断地晋升到年老代。最终老年代将没有更多的空间容纳新晋升的对象,此时引发 Major GC。

对象分配与晋升时何时会触发 GC 的详细流程图可以参考下图(参考了《码出高效:Java 开发手册》第四章走进 JVM 中的图):

​ 上图中没有描绘出 Thread Local Allocation Buffer (TLAB)与 Promotion Local Allocation Buffer (PLAB)的细节。此外上图中的 Full GC 可能让大家引起歧义,因为和 Major GC 太容易混淆了。实际 JVM 规范与垃圾收回相关的文献并没有给 Full GC 与 Major GC 作定义。一般 Full GC 认为是对新生代与老年代都进行垃圾回收,而 Major GC 则是专门针对年老代垃圾进行回收。那问题来了由 Young GC 引发了老年代的垃圾回收,是叫 Full GC 好呢,还是 Major GC 好呢?个人认为可能 Full GC 更合适,这个大家可以不用过多纠结这个。实现纠结可以看看这两篇文章Minor GC vs Major GC vs Full GCMajor GC 和 Full GC 的区别是什么?触发条件呢?

​ 上面只简单的描述了分代垃圾收集器垃圾收集的过程,实际垃圾收集器不仅负责了内存的回收工作,同样负责了对象的分配工作。更多的入门内容可以参考Memory Management in the Java HotSpot™ Virtual MachinePlumbr Handbook Java Garbage Collection。如果想再进一步了解垃圾回收相关的东西,还可以看看 《垃圾回收算法手册 自动内存管理的艺术》。

2.2 串行垃圾收集器

​ 串行垃圾收集器(Serial GC)在进行垃圾回收时只有单个 GC 线程在进行垃圾回收。通常实现串行垃圾回收器更加简单,串行垃圾回收器内部不用维护复杂的数据结构,内存开销也更加小。但由于在 STW(Stop The World)时只有单个 GC 线程在进行垃圾回收工作,垃圾回收的时间通常都会比较长,并且与应用程序占用的内存呈线性增长。该垃圾回收器比较适合 Client 端与嵌入设备等占用内存较小的场景。

​ 上图灰色箭头为应用线程,而黑色箭头为 GC 线程,应用线程在工作时通常都是多线程,而到过安全点后应用线程停止工作也叫 SWT(Stop The World),串行垃圾回收器将开始一个 GC 线程完成垃圾回收工作。根据回收分代的不同串行垃圾回收器通常又分为 Serial New 与 Serial Old,他们分别负责回收新生代(Young Generation)与年老代(Old Generation)。Serial New 采用复制算法完成垃圾清理工作,Serial Old 采用压缩算法完成垃圾清理工作。

2.3 并行垃圾收集器

​ 很显然在多核 CPU 架构下面垃圾回收时,串行垃圾回收器不能利用多核 CPU 的优势。因此出行了并行垃圾收集器(Parallel GC),其与串行垃圾回收器最大的差别在于 STW 时,进行垃圾回收的线程由单个变成了多个。相对于串行垃圾回收器而言,由于垃圾回收的工作被分配给了多个线程,每次进行 GC 时整体时间将大大下降。并行垃圾回收器工作时,新生代与年老代都会采用线程并行处理垃圾回收工作。与串行垃圾回收器一样,并行垃圾回收器,根据回收分代的不同通常又分为 ParNew 与 Parallel Old,他们分别负责回收新生代(Young Generation)与年老代(Old Generation)。同样新生代垃圾回收采用复制算法完成垃圾清理工作,年老代采用压缩算法完成垃圾清理工作。

​ 上图灰色箭头为应用线程,而黑色箭头为 GC 线程,并行垃圾回收器在 STW 时进行垃圾回收的线程相对于串行垃圾回收器而言变成了多个线程,并且这些线程同时进行垃圾回收工作。

2.4 并发标记清除垃圾收集器

​ 并发标记清除垃圾收集器 (Concurrent Mark Sweep,CMS )是 Hotspot VM 上真正意义上的并发垃圾回收器。所谓并发(Concurrent)是指 GC 线程与应用线程一起工作,GC 线程工作时不用 STW,应用线程也在工作,而通常说的并行(Parallel)是指多个 GC 线程同时工作,清理垃圾。很多文献中将应用线程叫作 Mutator Thread。

​ CMS 主要负责回收年老代垃圾,使用 CMS 时新生代垃圾收集工作通常由 Serial New 或 ParNew 完成,默认新生代垃圾回收器为 ParNew。CMS 回收年老代垃圾时,将整体垃圾回收的过程拆分为多个阶段,并且大部分阶段与应用线程都是并发不会发生 STW。CMS 整体垃圾回收过程可分为初始化标记( Initial-mark)、并发标记(Concurrent Marking)、并发预清除(Concurrent Pre-cleaning)、重新标记(Remark)、并发清除(Concurrent Sweeping),初始化标记与重新标记都会发生 STW,但通常时间都比较短。CMS 早其版本中初始化标记与重新标记都是由单线程完成的,后期版本可以通过 -XX:+CMSParallelInitialMark 与 -XX:CMSParallelRemarkEnabled 分别将初始化标记与重新标记阶段指定为多线程。在 CMS 对年老代进行并发回收时很多可能新生代发生了 Young GC,此时年老代垃圾回收将立刻中断,直到 Young GC 结束后又重新恢复。

​ 上图灰色箭头为应用线程,而黑色箭头为 GC 线程,CMS 在 Initial-mark 阶段开启多个 GC 线程对 GC Root 进行标记,该阶段通常时间会比较短。CMS 在并发标记与并发预清除阶段同会开启多线程工作,该阶段 GC 线程与应用线程并发工作。上图中 Concurrent Making Pre-cleaning 阶段中长的黑色箭头代表处理 Concurrent Making 工作的 GC 线程,短的黑色箭头代表处理 Pre-cleaning 工作的线程。CMS 在重新标记同样开启多个 GC 线程并且与 Initial-mark 阶段一样会 SWT。CMS 在并发清除阶段 GC 线程与应用线程并发工作。

​ CMS 调优的一个关键问题是如何找出合适的时间让 CMS 开始并发工作,以便在应用程序耗尽可用的堆空间之前 CMS 完成所有的并发工作。通常会某次 Young GC 后开始 CMS 的并发工作,因为 Young GC 过后 CMS Initail-mark 要标记的对象通常会更少。CMS 另外的一个问题是年老代内存碎片问题,由于 CMS 在回收年老代时采用了标记清除算法,标记清除算法相对于压缩算法而言执行效率更高,但由于清理垃圾时没有对内存的压缩整理,其不可避免地会出现内存碎片问题。下面二种情况会由于内存碎片问题最终导致 concurrent mode failure。

1、Young GC 时,Eden 区域存活的对象过大 Survivor 区域无法存放导致 promotion failed,此时对象只能放入年老代,但由于内存碎片问题年老代同样放不下该对象,最后将发生 concurrent mode failure,这时会引发 Full GC,Full GC 会回收整个 Heap 空间导致 STW 时长骤增。

2、Young GC 时,Survivor 区域存活对象年龄超过了 MaxTenuringThreshold,晋升到年老代,但由于内存碎片问题年老代放不下该对象,将发生 concurrent mode failure,这时会引发 Full GC。

更多关于 CMS 调优方面的实践可以参考这两篇文章 Java 中 9 种常见的 CMS GC 问题分析与解决Understanding GC pauses in JVM, HotSpot’s CMS collector

​ 下面是一张关于 HotSpot VM 中垃圾回收器如何组合分别处理年轻代与年老代的经典图。上面部分的 Serial New、ParNew、Parallel Scavenge 都是专门用于处理新生代垃圾收集器,下面部分的 CMS、Serial Old、Parallel Old 是专门用于处理年老代的垃圾收集器,而处于中间的 G1 即能处理新生代也能处理年老代。图中的黑色实线代表哪些新生代垃圾收集器能与哪些年老代垃圾收集器组合工作。CMS 与 Serial Old 之间的黑色虚线代表 CMS 发生 concurrent mode failure 时 fail safe 成 Full GC 采用 Serial Old 回收年老代垃圾。

​ 上面提到的 Serial GC(Serial New 与 Serial Old)、Parallel GC(ParNew、Parallel Scavenge、Parallel Old)、CMS,由于新生代与年老代其内存布局是连续的(虚似内存是连续的)这些垃圾收集器在回收垃圾时要么只能处理具体某一个分区要么只能处理整个 Heap。这必然会导致垃圾回收的 STW 时间或多或少与应用程序占用内存线性正相关,即应用程序占用的内存越大在执行垃圾回收时 STW 时间将越久。前面的垃圾收集器都是分代的垃圾收集器,G1 开启了分区垃圾收集器的先河(虽然 G1 在逻辑上也有新生代与年老代的概念)。G1 利用分治的思想将整体 Heap 划分为一块块大小相等的 Region,在内存管理时可以针对这些 Region 进行管理,而不是笼统地对某个 Generation 进行管理。由于 Region 的大小通常远小于 Generation,垃圾回收时处理多个 Region 效率通常高于处理某个 Generation。

3 G1 垃圾收集器概述

​ G1(Garbage First)垃圾收集器是续 CMS 收集器后的另一款跨时代的垃圾收集器,其开启了分区垃圾收集器的先河。G1 通过时间预测模型尽可能地满足用户对暂停时间的要求(用户可以通过-XX:MaxGCPauseMillis=XXX,来指定垃圾收回时最大的暂停时间),G1 利用压缩算法优化回收垃圾更多的分区,所以他被称作垃圾优先(Garbage First)垃圾收集器。

3.1 G1 垃圾收集中的内存布局

​ G1 与上面介绍的传统分代垃圾收集器一样同样存在 Eden Generation、Survivor Generation、Old Generation 的概念,但与他们最大的区别在于这些 Generation 的关系是逻辑上的关系,其各 Generation 内存布局不会存在连续性。G1 将 Heap 划分为一个个 Region,每个 Region 的大小为 2 的 N 次方,其值在 1M 到 32M 之间。每一个 Region 属于某个 Generation,于是有了 Eden Region、Survivor Region、Old Region/Tenured Region 的概念(不像传统分代垃圾收集器,G1 中没有 From Survivor 与 To Survivor 的概念, 因为 G1 不管是 Young GC、Mix GC、Full GC 对象都是从一个 Region 转移到另外一个 Region 或是直接清除)。除此之外 G1 还有一个专门用于存放大对象的 Region(默认对象占用内存超过 Region 大小二分之一的对象),称为 Humongous Region,Humongous Region 可能由多个 Region 构成,但一个 Region 最多存放一个大对象,当多个 Region 用于存放一个特别大的对象这些 Region 内在布局上是连续的。当经过多次 Young GC、Mix GC、Full GC 与对象分配后(G1 中 Young GC、Mix GC、Full GC 相关的东西后面会涉及),Eden Region、Survivor Region、Old Region、Humongous Region 之间的角色会转变,即原来存有具体某种 Generation 对象的 Region 被清空后可以用来存放 Eden 对象、Survivor 对象、Old 对象或是 Humongous 对象中的某一种。

​ 这种将内存分为一个个 Region 的内存布局更加有利于内存的回收,垃圾回收集可以采用分治的思想去管理一小块的内存(处理内存的分配与回收),避免了之前版本垃圾回收集在处理 Old Generation 时只能处理整个 Old Generation 困局(整个 Old Generation 一起处理通常非常耗时的,而且这个过程中避免不了 STW)。

3.2 G1 垃圾收集的周期

​ 从全局视角来看,G1 收集器回收垃圾的过程是在 Young-only 阶段与 Space Reclamation 阶段之间进行交替的,下图来源于 Oracle 官网HotSpot Virtual Machine Garbage Collection Tuning Guide 一文中。

  • Young-only 阶段

    Young-only 阶段实际包括了多次 Young GC 与整个并发标记过程。其从一些普通的 Young GC(上图中小的蓝色点代表普通的 Young GC)开始,并将满足条件的对象提升到老年代。当年老代占用内存超过阈值时,会触发并发标记阶段,该阈值由参数-XX:InitiatingHeapOccupancyPercent=65%,指定默认值为 65%。与此同时 G1 会开启并发的 Young GC(上图中大的蓝色大代表并发的 Young GC) ,而不是普通的 Young GC。整个并发标记阶段是与普通的 Young GC 交替的。并发标记阶段又可细分为初始化标记(Initial Marking)、重新标记(Remark)与清理(Cleanup)阶段。初始化标记阶段实际是在并发的 Young GC 中完成的(文献中通常用 piggybacking 一词表述)。当初始化标记完成后可能会发生若干的普通 Yong GC,才进入 Remark 阶段(上图中靠上方的黄色小点)。接着并发标记可能被 Young GC 打断,Young GC 结束后再进入 Cleanup 阶段(上图中靠下方的黄色小点)。

  • Space Reclamation 阶段

    当 Cleanup 结束后,G1 会进入 Space Reclamation 阶段,该阶段由若干次的 MixGC 组成。每次 MixGC 都会从之前并发标记阶段标记的对象中选择一部分进行清理,MixGC 过程中同时伴随着部分的 Young GC(上图中红色的小点代表一次 MixGC)。当 G1 发现清除对象所获取的空间不够多时将停止 MixGC,与此同时 Space Reclamation 阶段结束。

当 Space Reclamation 阶段结束后,G1 收集周期又重一个 Young-only 阶段重新开始。Young-only 中的普通 Young GC 的触发条件与前分的分代垃圾收集器 Young GC 触发条件一致,只不过 G1 是针对 Region 处理的,即 G1 会根据 Eden Region 中是否有 Region 能够容纳新对象来决定是否要开启 Young GC。作为兜底策略,当 G1 垃圾回收过程释放的内存不足于满足应用程序中新对象对内存要求时,G1 会采用 Full GC 处理所有 Region。

3.3 记忆集(RSet)

​ 前面已了解到 Young GC 时只会处理新生代对应的 Region 即 Eden Region 与 Survivor Region,这有利于降低每次 Young GC 的时间。但如果 Eden Region 与 Survivor Region 持有老年代的引用呢,难道在 Young GC 时,要把 Heap 中所有的 Region 都遍历一次才能确定 Eden Region 与 Survivor Region 有哪些对象才是垃圾吗?这种方式显然是不可取的,这样一来就会拉长 Young GC 的时间。

​ 有种有效地方法是新生代的每一个 Region 都维护一个集合记录一下老年代指进来的(point-in)的跨代引用,这样在 Young GC 时只要看一下这个 point-in 的集合就行,这个集合便是所谓的记忆集(Remember Set,RSet)。那年老代里面需要这个 RSet 吗?前面提到每次 Mix GC 时会回收部分年老代的 Region,如果没有这个记忆集的话和 Young GC 一样同样避免不了要扫描整个年老代的 Region,所以年老代的 Region 也要维护一个 point-in 的集合,不过个集合记录是 Old Region point-in 过来的集合,至于 Young Region point-in 过来的则可以不用管。

​ 那 RSet 具体实现上又是怎么样的呢?在这之前必须先知道卡表(CardTable),在 G1 之前 CMS 中也有 CardTable。CardTable 本质上是一种 point-out 数据结构,表示某一区域自己有指向别的区域的引用。在 G1 中 CardTable 由 byte 数组构成,数组的每个元素称之为卡片/卡页(CardPage)。CardTale 会映射到整个堆的空间,每个 CardPage 会对应堆中的 512B 空间。如下图所示,在一个大小为 8GB 的堆中,那么 CardTable 的长度为 16777215 (8GB / 512B);假设-XX:G1HeapRegionSize 参数为 2MB,即每个 Region 大小为 2 MB,则每个 Region 都会对应 4096 个 CardPage。CardTable 将占用 16MB 额外内存空间。

查找一个对象所在的 CardPage 只需要应用如下公式便可得出。

CardPageIndex=(对象的地址–堆开始地址)÷512CardPageIndex = (对象的地址 – 堆开始地址) ÷ 512CardPageIndex=(对象的地址–堆开始地址)÷512

​ 说完 CardTable 再来看看 RSet 的具体实现,RSet 实际是通过 HashMap 实现的,该 HashMap 其 key 引用了本 Region 的其他 Regionr 的地址,value 是一个数组,数组的元素是引用方的对象所对应的 CardPage 在 CardTable 中的下标。

如上图所示,区域 B 中的对象 y 引用了区域 A 中的对象 x,这个引用关系跨了两个区域。y 对象所在的 CardPage 为 179,在区域 A 的 RSet 中,以区域 B 的地址作为 key,b 对象所在 CardPage 下标 79 为 value 记录了这个引用关系,这样就完成了这个跨区域引用的记录。不过这个 CardTable 的粒度有点粗,毕竟一个 CardPage 有 512B,在一个 CardPage 内可能会存在多个对象。所以在扫描标记时,需要扫描 RSet 中关联的整个 CardPage,上图的例子是要把 CardTable 下标为 79 的 CardPage 都扫描一遍。

​ 实际上 HotSpot VM 中 G1 的 RSet 具体实现要比上面说的更加复杂(上面说的只是其中的一种情况,Sparse 粒度的情况 )。应用程序中可能存在频繁的更新引用情况,这会使得某些区域的 RSet 变成 popular Region。G1 采用不同粒度的方式来处理 RSet Popularity,RSet 可分为 Sparse、Fine、Coarse 三种粒度。不同粒度时 RSet 内部采用不同的数据结构记录其他 Region point-in 进来的引用 ,上面介绍的便是 Sparse 粒度时的情况。下面是 Evaluating and improving remembered sets in the HotSpot G1 garbage collector论文中给出的 G1 中 RSet 数据结构的简化定义。

1、Sparse Grained (上面 g1_rset 数据结构中的 saprse)

稀疏粒度情况时,采用 HashMap 实现,该 HashMap 其 key 引用了本 Region 的其他 Regionr 的地址,value 是一个数组,数组的元素是引用方的对象所对应的 CardPage 在 CardTable 中的下标。

2、Fine Grained (上面 g1_rset 数据结构中的 fine_grained)

细粒度情况时,同样采用 HashMap 实现,该 HashMap 其 key 引用了本 Region 的其他 Regionr 的地址,value 是一个位图,位图的最大位数代表一个 Region 最多能被拆分为多少 CardPage,位图上值为 1 则代表 Region 上 CardPage 内有对象引用了 RSet 所属 Region 的对象。

3、Coarse Grained (上面 g1_rset 数据结构中的 coarse)

粗粒度情况时,采用位图实现,位图的最大位数代表整个 Heap 能被拆分为多少个 Region。位图上值为 1 则代表其他 Region 内有对象引用了 RSet 所属 Region 的对象。因为 Region 的大小是一样的,可以通过 Heap 的起始地址,计算出位图中每个 Region 的起始地址。

G1 通常利用 Refinement Threads 异步维护 RSet,每个线程会利用前面介绍的 post-write barrier 将跨代引用与 Old Generation 到 Old Generation 的引用记录到各自的 local log buffer 中,当 local log buffer 满了之后会刷新到全局的 log buffer 中,Refinement Threads 专门处理全局的 log buffer 来维护 RSet,当 Refinement Threads 不能有效地处理全局的 log buffer 时,应用线程将一起处理 log buffer,但这对应用线程的性能有损耗。当垃圾回收过程中如果全局的 log buffer 还未处理完,GC 线程将处这些 log buffer。

scss

复制代码

void oop_field_store(oop* field, oop new_value) { pre_write_barrier(field); // pre-write barrier: for maintaining SATB invariant *field = new_value; // the actual store post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference }

3.4 回收集(CSet)

​ 回收集(Collection Set,CSet),其代表每次 GC 暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中。

4 深入分析 G1 垃圾收集

​ 前面已简要地介绍了,G1 中 Heap 的内存布局、全局视角下 G1 的周期、RSet 具体实现、CSet 等内容,下再更细致地介绍一下 G1 中的 Young GC 阶段、并发标记阶段、Mix GC 阶段。

4.1 Young GC 阶段

​ 同分代垃圾回收器一样,当 G1 中没有 Eden Region 能够容纳新要创建的对象时,G1 中 Young GC 被触发;同时每个线程都有对应的 TLAB,小的对象优先直接在 TLAB 中创建。前面已了解到 G1 的 Young GC 阶段只会回收全部的 Young Region,Eden Region 与 Survivor Region;同时如果年老代内存占比超过了指定的阈值时,Young GC 会一同完成并发标阶段的初始化标记工作。每次 Young GC 后,G1 会根据当前新生代大小、新生代最小值、新生代最大值、目标暂停时间等重新调整新生代的大小。下面通常 Young GC 的 GC 日志看一下 Young GC 具体包括那些阶段。JDK 的采用的是 HotSpot 1.8.0_241 版本的 JDK,JVM 参数如下:

-XX:+UseG1GC -XX:G1HeapRegionSize=2m -Xms2g -Xmx2g -Xloggc:/Users/mac/Desktop/g1log -XX:+PrintGCDetails

-XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps

java

复制代码

2021-12-29T10:03:58.217-0800: 0.244: [GC pause (G1 Evacuation Pause) (young), 0.0914253 secs] [Parallel Time: 90.3 ms, GC Workers: 8] [GC Worker Start (ms): Min: 244.0, Avg: 244.1, Max: 244.1, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.7, Diff: 0.7, Sum: 2.2] [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0] [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2] [Object Copy (ms): Min: 89.1, Avg: 89.6, Max: 89.8, Diff: 0.7, Sum: 716.5] [Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8] [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8] [GC Worker Other (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.1, Sum: 1.3] [GC Worker Total (ms): Min: 90.1, Avg: 90.1, Max: 90.2, Diff: 0.1, Sum: 721.0] [GC Worker End (ms): Min: 334.2, Avg: 334.2, Max: 334.2, Diff: 0.1] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.1 ms] [Other: 1.1 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.7 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.1 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms] [Eden: 102.0M(102.0M)->0.0B(88.0M) Survivors: 0.0B->14.0M Heap: 102.0M(2048.0M)->98.0M(2048.0M)] [Times: user=0.12 sys=0.28, real=0.09 secs]

如上 GC 日志第一行所示 GC pause (G1 Evacuation Pause) (young) 代表本次 GC 暂停为 G1 的 Young GC。Young GC 的工作分为并行工作与其他工作,为别为 GC 日志中的 [Parallel Time: 90.3 ms, GC Workers: 8][Other: 1.1 ms]。并行工作是 Young GC 的主要工作内容,并行工作被拆分为如下几部分。

1、External Root Scanning(GC 日志中的 [Ext Root Scanning (ms): …部分)

负责处理扫描指向 CSet 的外部根,例如寄存器、线程堆栈等。

2、Update Remembered Sets (RSets) (GC 日志中的 [Update RS (ms): …部分)

负责更新 RSet,RSet 之前详细介绍过主要是用来记录别的 Region point-in 进来的引用。

3、Processed Buffers(GC 日志中的[Processed Buffers: …部分)

前面说过 RSet 的维护是通过先写 log buffer 然后再更新,Processed Buffers 便是处理那些在 Young GC 开始后还没有被 Refinement Thread 处理完的 log buffer,这保证的 RSet 的完整性。

4、Scan RSets(GC 日志中的[Scan RS (ms): …部分)

负责扫描 RSets 中其他 Region 指向本 Region 的引用。这个扫描时间会因为 RSet 的 Popularity 不同采用不同的粒度的数据结构存储而相差很多;前面介绍过的 Coarse Grained 时 RSet 的扫描时间将最耗时。

5、Code Root Scanning(GC 日志中的[Code Root Scanning (ms): …部分)

负责扫描 CSet 中已编译源代码的引用根。

6、Object Copy (GC 日志中的[Object Copy (ms): …部分)

负责将新生代 Region,即 Eden Region 与 Survivor Region 中依赖存活的对象复制到未使用的 Survivor Region 中或者将晋升的对象复制到 Old Region 中。

7、Termination (GC 日志中的[Termination (ms): …与 [Termination Attempts (ms): …)部分

当每个 GC Work 线程完成其自身的工作后,会进行了结束阶段,这时已完成工作的 Work 线程会与其他 Work 线程同步,同时尝试采用工作窃取算法获取还未完成工作的其他线程的工作。Termination Attempts 部分代表 Work 线程成功获取工作,处理完后再次尝试结束。这个过程其还会再次尝试获取其他未完成工作线程的任务。

其他工作,这部分主要是一些他们的任务包括选择 Regino 进入 CSet、引用处理、引用入列队、重新标记卡页为脏页、释放 CSet、处理 Humongous 对象等。关于 G1 GC 日志更详细的解释可以参考Collecting and reading G1 garbage collector logs - part 2

​ 当年老代内存占比超过了-XX:InitiatingHeapOccupancyPercent 指定的阈值,Young GC 会顺便完成并发标记的初始化标记工作。这时在 GC 日志中将出现 GC pause (G1 Evacuation Pause) (young) (initial-mark) 的关键信息,其中的 initial-mark 代表在进行 Young GC 时稍带完成的初始化标记工作。

4.2 G1 中的 SATB 具体实现

​ 文章的最开始已介绍过垃圾收集器通常会采用 Tri-color Marking 算法来处理标记阶段对象引用的分析过程,在处理漏标问题上 G1 采用了 Yuasa 在Real-Time Garbage Collection on General Purpose Machines 提出的“snapshot-at-the-beginning” (SATB) 算法。

​ SATB 算法确保在并发标记开始后所有的垃圾对象都通过快照被识别出来。在并发标记过程中新分配的对象被认为是存活的对象,不用对他们进行追踪分析,这有利于减小标记的开销。G1 维护二个用于并发标记的全局 bitmap,分别被标记为 previous 与 next。previous 位图中保存了前一次并发标记的标记信息,next 位图保存了当前正在进行或刚完成并发标记的标记信息。previous bitmap 中上次并发标记的标记信息,在本次本发标记中可以直接使用。同时每个 Region 都有几个重要的指针 PTAMS(上一次并发标记的起始位置)、NTAMS(下一次并发标记的起始位置)、Bottom(Region 的起始地址)、Top(Region 已使用地址)、End(Region 的结束地址);TAMS 实现是 top at mark start 的缩写,也就是每次并发标记会把对应的指针放在 Top 针指同一位置,代表标志的结束位置 。每次并发标记开始时,NTAMS 指针重新指 Top 指针的位置,当并发标记结束后,NTAMS 指针与 PTAMS 指针会交位置,next bitmap 与 previous bitmap 交换角色,新的 next bitmap 被清空,即原来的 previous bitmap 被清空。

上图展示了某次并发标记过程中一个 Region 中 Bottom、PTAMS、NTAMS、Top、End 指针位置,指针之间区域的含义。区间中的白色、灰色、黑色可以大致理解为三色标记法中的三种颜色。

[Bottom, PTAMS) 区间

该区间代表上次并发标记的区间,PTAMS 为上次并发标记的结束位置,该区间上次并发标记的信息能直接被正在进行的并发标记利用,即正在进行的并发标记通过上次 bitmap 知道该区间哪些是垃圾哪些是存活对象。

[PTAMS, NTAMS) 区间

该区间代表本次正在进行的并发标记的区间,NTAMS 为本次并发标记的结束位置,在并发标记开始时 G1 会为[PTAMS, NTAMS) 区间创建一个快照,实际就是 next bitmap,然后处理 bitmap 映射的地址,标记这些地址上的对象是垃圾还是存活的。实际标记过程就是有一个指针从 PTAMS 指针位置一直移到 NTAMS 指针位置。

[NTAMS, Top) 区间

该区间代表并发标记过程中,应用线程新生成的对象,前面已说过在并发标记过程中新分配的对象被认为是存活的对象,所以上图中该区间全是黑色的。并发标记刚开始时 Top 指针与 NTAMS 指针处于同一位置,当应用线程每生成一个新对象时,Top 指针就会相应的向 End 指针的方向右移。

[Top, End) 区间

该区间代表 Region 中还没有使用的空间。

很显然 GC 线程只会去处理**[PTAMS, NTAMS) 区间完成标记工作,而应用线程运行则会对[Bottom, Top)区间有影响。应用线程对[Bottom, Top)区间[NTAMS, Top)区间的影响并不会影响 GC 线程的并发标记工作,因为该部分应用线程新增的对象都认为是存活的对象。应用线程对[Bottom, Top)区间[PTAMS, NTAMS)区间的影响可能会影响 GC 线程的并发标记工作,G1 通过前面介绍的 pre-write barrier 来确保标记的正确性,即如果应用线程在[PTAMS, NTAMS)区间内增加了黑色对象对白色对象的引用,pre-write barrier 内部处理时会将白色对象设置为灰色对象,使得该对象能再次被标记不会产生漏标。应用线程对[Bottom, Top)区间[Bottom, PTAMS)区间**的影响可能会影响 GC 线程的并发标记工作,具体 G1 是如何处理这个有待考证,猜测应该也是利用 write barrier 这里的技术。

​ 有了上面介绍,再看一下 Sun 公司 G1 的论文Garbage-First Garbage Collection 中 Initial Marking Pause/Concurrent Marking 小节中的这个图应该会清晰点。

Initial Marking 阶段,当 Region 首次被标记时,PrevBitmap 为空,NextBitmap 中有**[PrevTAMS, NextTAMS)区间**的块照,并发标记结束后将确定 Bitmap 中哪些是垃圾对象, PrevTAMS 指针与 Bottom 指针位置相同,NextTAMS 指针与 Top 指针位置相同。

Remark 阶段,[PrevTAMS, NextTAMS)区间的存活对象与垃圾对象被标记出来,NextBitmap 发生改变其中黑色部分表示标记出来的存活对象,白色部分为垃圾对象。同时由于应用程序生成了新的对象,Top 指针的位置从 NextTAMS 指针处向右移动了。

Cleanup/GC Pauses 阶段,NextBitmap 与 PrevBitmap 互换角色,同时 NextTAMS 指针与 PrevTAMS 指针互换位置。

新一轮标记 Initial Marking 阶段,NextTAMS 指针重新指向 Top 指针,PrevBitmap 保证了上一次的标记信息,NextBitmap 中有**[PrevTAMS, NextTAMS)区间**的块照。

新一轮标记 Remark 阶段,再重复上面 B 的事情。

新一轮标记 Remark 阶段,Cleanup/GC Pauses 阶段,再重复上面 C 的事情。

4.3 并发标记阶段

​ 并发标记阶段主要是将 Mix GC 时要收集的垃圾对象先进行标记,然后根据 Region 能释放的内存空间做一下排序,同时其会在标记的最后阶段直接释放那些没有存活对象的 Region,并将这些 Region 加入到可用 Region 列表中。并发标记阶段可细分为 Initial Mark、Root Region Scanning、Concurrent Marking、Remark、Cleanup 等等五个阶段。

Initial Mark 阶段

当年老代内存占比超过 -XX:InitiatingHeapOccupancyPercent 指定的阈值时会触发并发标记,并发标记的第一个阶段为 Initial Mark,该阶段会 STW,其只扫描 GCRoot 直接引用的对象,由于 Young GC 时也要扫描 GCRoot 直接引用的对象,Young GC 时会顺便完成 Initial Mark 的工作。GC 日志通常会有GC pause (G1 Evacuation Pause) (young) (initial-mark) 的关键信息,其中的 initial-mark 代表在进行 Young GC 时顺便完成的初始化标记工作。

Root Region Scanning 阶段

实际扫描的是新生代 Survivor Region 引用的对象,该阶段必须在下次 GC 暂停前完成,因为 Heap 要扫描存活对象的话,Survivor Region 引用的对象必须先被识别。

Concurrent Marking 阶段

并发标记阶段 GC 线程与应用线程是并发的,同时可以通过-XX:ConcGCThreads 指定并行 GC 线程数。前面已介绍过 G1 采用 pre-write barrier 解决并发标记过种中因为应用线程更新了并发开始阶段创建的对象图的快照导致的漏标问题,每个线程。并发标记阶段会顺带完成每个 Region 对象的计数工作,方便后面统计哪些 Region 能回收更多的内存。

Remark 阶段

该阶段实际是标记的最后阶段,其会 SWT,这个阶段就负责把剩下的引用处理完,该阶段会处理之前 SATB write barrier 记录的尚未处理引用。但其与与 CMS 的 remark 有本质的区别,即 G1 的 Remark 的暂停只需要扫描 SATB buffer,而 CMS 的 remark 需要重新扫描里全部的 dirty card 外加整个根集合,而此时整个新生代都会被当作根集合的一部分,因而 CMS remark 有可能会非常慢。

Cleanup 阶段

该阶段会阶段会 STW,其主要工作是重置标记状态,如前面介绍的 NextBitmap 与 PrevBitmap 互换角色,同时 NextTAMS 指针与 PrevTAMS 指针互换位置。同时若发现有 Region 没有存活对象,则会直接将 Region 清空并将 Region 加入到空闲 Region 列表中。当然统计每个 Region 能回收多少垃圾的统计工作也在这个阶段完成,这样后 Mix GC 对象转移时便能快速地确定 CSet。

4.4 Mix GC 阶段

​ MixGC 阶段主要负责回收部分年老代与全部新生代的 Region,G1 会根据设置的目标暂停时间-XX:MaxGCPauseMillis 将并发标记阶段标记好的 Region,按其可以释放内存空间大小,依次进行回收,即一个 MixGC 阶段会包含多次的 MixGC,当 G1 发现释放垃圾对象获取的内存空间过小时其将停止 MixGC。MixGC 时在 GC 日志中将出现**GC pause (G1 Evacuation Pause) (mixed)**的关键信息。作为兜底策略,当 G1 垃圾回收过程释放的内存不足于满足应用程序中新对象对内存要求时,G1 会采用 Full GC 处理所有 Region。

总结

​ 本文详细深入地分析了 G1 垃圾收集器底层的实现原理,虽然没有涉及 G1 垃圾收集器的具体源码但基本把 G1 相关的知识都由浅及深地分析了一下。希望看完本文你会有所收获,限于本人能力有限文中不正确还望指正。最后欢迎关注个人公众号洞悉源码。

参考

Java Performance Companion

《垃圾回收算法手册自动内存管理的艺术》

Memory Management in the Java HotSpot™ Virtual Machine

Plumbr Handbook Java Garbage Collection

Java Garbage Collection Basics

G1: One Garbage Collector To Rule Them All

HotSpot Virtual Machine Garbage Collection Tuning Guide

Collecting and reading G1 garbage collector logs - part 2

[HotSpot VM] 请教 G1 算法的原理

Java 虚拟机 07 - 垃圾收集器之 G1

Garbage-First Garbage Collection

Minor GC vs Major GC vs Full GC

Real-Time Garbage Collection on General Purpose Machines

memorymanagement.org Tri-color Marking

Wiki Tri-color Marking

On-the-Fly Garbage Collection: An Exercise in Cooperation

Uniprocessor Garbage Collection Techniques

Evaluating and improving remembered sets in the HotSpot G1 garbage collector

面试官问我 G1 回收器怎么知道你是什么时候的垃圾?

结尾

​ 原创不易,点赞、在看、转发是对我莫大的鼓励,关注公众号洞悉源码是对我最大的支持。同时相信我会分享更多干货,我同你一起成长,我同你一起进步。

分类:

后端

标签:

JVM后端

相关小册

「从零开发企业级 Go 应用」封面

从零开发企业级 Go 应用

孔令飞

1070 购买

¥29.94

¥49.9

618 大促

「RocketMQ 核心原理解析」封面

RocketMQ 核心原理解析

[SH 的全栈笔记 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QRuZeB3n-1686816243944)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8584543d8535435a9d74c1fbf7901ac7~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp “创作等级LV.5”)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0u1iepXQ-1686816243945)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/ffdbad884aa0e7884cbcf924226df6ce.svg “VIP.3 渐入佳境”)]](/user/3509296845029384)

1148 购买

¥23.94

¥39.9

618 大促

评论

表情

图片

Ctrl + Enter 发表评论

全部评论 12

最新

最热

stanwang的头像

删除

stanwang [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kR4cktn-1686816243946)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53901be7d27e4abe94530e12d815491d~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp “创作等级LV.2”)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P2IgbIpd-1686816243946)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/65a6a28f15d70e5a77bf881c5ec5340d.svg “掘友3级:新星掘友”)]

1 年前

先点赞再看~

展开

收起

点赞

回复

用户9402621043623的头像

删除

用户 9402621043623 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6z6rsDwD-1686816243947)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/9b2f31c663d17de59dd9e5fff272bb85.svg “掘友1级:预备掘友”)]

1 年前

先感谢楼主的文章!收益很多!同时也提醒楼主,关于 G1 在“Young-Only"和”Space Reclamation"两个阶段循环这块,楼主引用的 Oracle 文档已经更新了,把 Concurrent Young 改成了 init Mark,这就跟 G1 回收的六个阶段对应上了,图示和解释都做了更新,附上链接 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-osEqfi78-1686816243947)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/3f843e8626a3844c624fb596dddd9674.svg)]docs.oracle.com
最后再次感谢楼主,在本文的基础上看官方文档更容易了!

展开

收起

点赞

1

删除

叶易

(作者) 1 年前

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5pqHPaY2-1686816243948)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_2.cd1e2bd.png)]谢谢支持与指正

展开

收起

点赞

回复

user1090529752219的头像

删除

user1090529752219 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y5eHn1sz-1686816243948)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/8c6985e2aa4c06f307ae3734da4b43ac.svg “掘友4级:进阶掘友”)]

1 年前

很厉害,赞!!!

展开

收起

点赞

回复

撒库拉给的头像

删除

撒库拉给 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YwRCzq8U-1686816243948)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/8c6985e2aa4c06f307ae3734da4b43ac.svg “掘友4级:进阶掘友”)]

:) 1 年前

太长了[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vmd3NOG3-1686816243949)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_9.8cf4c38.png)]

展开

收起

点赞

回复

爱掘金的祢豆子的头像

删除

爱掘金的祢豆子 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nA7YOnw1-1686816243949)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/922e26916a444513bceddad5bcf437e1~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp “创作等级LV.3”)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rzglh3Iz-1686816243950)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/cf19b122213ebdbd25b66f02500b8c27.svg “掘友5级:先锋掘友”)]

公众号[摸鱼小助手],欢迎关注~ 1 年前

良心啊

展开

收起

点赞

2

删除

叶易

(作者) 1 年前

没人看呀,哈哈,我以后全当笔记啦

展开

收起

1

回复

删除

XQDD

回复

叶易

1 年前

很有用!

展开

收起

没人看呀,哈哈,我以后全当笔记啦

点赞

回复

spoofer的头像

删除

[spoofer [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EcXtSAj-1686816243951)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a02c571ff5e24b90b0c7aa70c58ebd1c~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp “创作等级LV.1”)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GalNUjl4-1686816243951)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/8c6985e2aa4c06f307ae3734da4b43ac.svg “掘友4级:进阶掘友”)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IzNZEVA2-1686816243952)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/ffdbad884aa0e7884cbcf924226df6ce.svg “VIP.3 渐入佳境”)]](/user/2928754709772615)

后台研发工程师 @ 某 K12 教育科技公司 1 年前

看你画了那么多图,不得不点个赞啊[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QrTiNaZq-1686816243952)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_2.cd1e2bd.png)]

展开

收起

点赞

1

删除

叶易

(作者) 1 年前

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aVfRd7r1-1686816243953)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_2.cd1e2bd.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNtVeeK8-1686816243954)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/jj_emoji_2.cd1e2bd.png)]

展开

收起

1

回复

叶易的头像

删除

叶易 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MxJf2Dnx-1686816243956)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/922e26916a444513bceddad5bcf437e1~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp “创作等级LV.3”)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sd31WZh0-1686816243956)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/8c6985e2aa4c06f307ae3734da4b43ac.svg “掘友4级:进阶掘友”)]

(作者) java 工程师 @ 百度中国 1 年前

觉得好给点个赞,帮我升升等级,后期再发原创文章推广权限会大点

展开

收起

1

回复

小徐同学啊的头像

删除

小徐同学啊 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8gGz2nVt-1686816243957)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/8c6985e2aa4c06f307ae3734da4b43ac.svg “掘友4级:进阶掘友”)]

研发 1 年前

非常详细,豁然开朗,厉害

展开

收起

点赞

回复

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OU8DvBV1-1686816243957)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/00ba359ecd0075e59ffbc3d810af551d.svg)] 25

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNn02Ttr-1686816243958)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/336af4d1fafabcca3b770c8ad7a50781.svg)] 12

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TPk2Kj0M-1686816243958)(//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e371749f308e0d99430a6a4943eef946.svg)] 已收藏

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值