乐趣区

关于jvm:这可能是最清晰易懂的-G1-GC-资料

概述

G1 (Garbage-First) 于 JDK 6u14 版本公布,JDK 7u4 版本发行时被正式推出,在 JDK9 时曾经成了默认的垃圾回收器,算是 CMS 回收器的代替 计划(CMS 在 JDK9 当前曾经废除)

G1 是一款分代的 (generational),增量的 (incremental),并行的 (parallel),移动式(evacuating)的,软实时的垃圾回收器。其最大特点是暂停工夫可配置,咱们能够配置一个最大暂停工夫,G1 就会尽可能的在回收的同时保障程序的暂停工夫在容许范畴内,而且在大内存环境下体现更好。

在正式介绍 G1 细节之前,先简略说说垃圾回收的一些基本知识:

垃圾回收的一些基础知识

mutator

在垃圾回收的里,mutator 指应用程序。至于为什么叫这个奇怪的名字?

mutator 是 Edsger Dijkstra 推敲进去的词,有“扭转某物”的意思。说到要扭转什么,那
就是 GC 对象间的援用关系。不过光这么说可能大家还是不能了解,其实用一句话概括的话,它的实体就是“应用程序”。这样说就容易了解了吧。GC 就是在这个 mutator 外部精神饱满地 工作着。

增量垃圾回收

增量式垃圾回收(Incremental GC)是一种通过逐步推动垃圾回收来管制 mutator 最 大暂停工夫的办法。

像一些晚期的年老代垃圾回收器,都是齐全暂停的,比方 Serial GC
简略的说,增量垃圾回收,就是让 GC 程序和 Mutator 交替运行的方法,交替执行时,实际上垃圾是一点点回收的,所以叫“增量(Incremental)”

G1 就属于一款增量垃圾回收器,它通过和 mutator 交替运行的形式来升高因 GC 导致的程序暂停工夫

并行 GC 和并发 GC

并行 GC(Parallel)和并发(concurrent)GC 是垃圾回收里的一个基本概念。这两个词很含糊,容易弄混同,不过在 GC 的畛域里,就用 GC 里的解释吧。

一般来说,以多线程执行的 GC被称为并行 / 并发 GC,不过这两个词在 GC 里意思齐全不同。

并行的 GC 会先暂停 mutator,而后开启多个线程并行的执行 GC,如下图所示:

而并发 GC 是在不暂停 Mutator 运行的同时,开启 GC 线程并行的执行 GC,如下图所示:

并行 GC 的目标是 晋升 GC 效率,缩短暂停工夫 ,而并发 GC 的目标是彻底 干掉暂停工夫

可预测

G1 的执行流程

G1 GC 中的堆构造和其余回收器的有所不同,在 G1 中,堆被划分为 N 个大小的相等的区域(Region),每个区域占用一段间断的地址空间,以区域为单位进行垃圾回收,而且这个区域的大小是可配置的。在调配时,如果抉择的区域曾经满了,会主动寻找下一个闲暇的区域来执行调配。

G1 是一个分代的垃圾回收器,同样的它将堆分为年老代(young)和老年代(old),将划分的区域又分为年老代区域和老年代区域。不过和其余垃圾回收器不同,G1 中不同代的区域空间并不是间断的。

这里解释一下,为什么 G1 中不同代应用不间断的区域。因为 G1 Heap 中初始时只划分了区域,并没有给区域分类,在对象调配时,只须要从闲暇区域集(free-list)中选取一个存储对象即可,这样区域调配更灵便。

当产生 GC 时,EDEN 区被清空,而后会作为一个闲暇的区域,这个区域待会可能会作为老年代,也可能会被作为 Survivor 区域。不过在 G1 在调配时还是会查看新生代的区域总大小是否超过新生代大小限度的,如果超出就会进行 GC

尽管每个区域的大小是无限的,不过针对一些占用较大的大对象(humongous object),还是会存在跨区域的状况。对于跨区域的对象,会调配多个间断的区域。

G1 中的区域,次要分为两种类型:

  1. 年老代区域

    1. Eden 区域 – 新调配的对象
    2. Survivor 区域 – 年老代 GC 后存活但不须要降职的对象
  2. 老年代区域

    1. 降职到老年代的对象
    2. 间接调配至老年代的大对象,占用多个区域的对象

G1 中的堆构造如下图所示:

和其余的垃圾回收形式有所不同,G1 的年老代 / 老年代的回收算法都是统一的,属于挪动 / 转移式回收算法。比方复制算法,就属于移动式回收算法,长处是没有碎片,存活的越少效率越高

RememberedSet

比方在对某个区域进行回收时,首先从 GC ROOT 开始遍历 可中转这些区域中 的对象,可因为 降职或者挪动的起因 ,这些区域中的某些对象挪动到了其余区域, 可是挪动之后依然放弃着对原区域对象的援用;那么此时原区域中被援用的对象对 GC ROOT 来说并不能“中转”,他们被其余对象的区域援用,这个发动援用的其余对象对于 GC ROOT 可达。这种状况下,如果想正确的标记这种 GC ROOT 不可中转但被其余区域援用的对象时就须要遍历所有区域了,代价太高。

如下图所示,如果此时堆区域 A 进行回收,那么须要标记区域 A 中所有存活的对象,可是 A 中有两个对象被其余区域援用,这两个灰色的问号对象在区域 A 中对 GC ROOTS 来是不可达的,然而实际上这两个对象的援用对象被 GC ROOTS 援用,所以这两个对象还是存活状态。此时如果不将这两个对象标记,那么就会导致标记的脱漏,可能造成误回收的问题

RememberedSet(简称 RS 或 RSet)就是用来解决这个问题的,RSet会记录这种跨代援用的关系。在进行标记时,除了从 GC ROOTS 开始遍历,还会从 RSet 遍历,确保标记该区域所有存活的对象(其实不光是 G1,其余的分代回收器里也有,比方 CMS)

如下图所示,G1 中利用一个 RSet 来记录这个跨区域援用的关系,每个区域都有一个 RSet,用来记录这个跨区援用,这样在进行标记的时候,将 RSet 也作为 ROOTS 进行遍历即可

所以在对象降职的时候,将降职对象记录下来,这个存储跨区援用关系的容器称之为 RSet,在 G1 中通过 Card Table 来实现。

留神,这里说 Card Table 实现 RSet,并不是说 CardTable 是 RSet 背地的数据结构,只是 RSet 中存储的是 CardTable 数据

Card Table

在 G1 堆中,存在一个 CardTable 的数据,CardTable 是由元素为 1B 的数组来实现的,数组里的元素称之为卡片 / 卡页(Page)。这个 CardTable 会映射到整个堆的空间,每个卡片会对应堆中的 512B 空间。

如下图所示,在一个大小为 1 GB 的堆下,那么 CardTable 的长度为 2097151 (1GB / 512B);每个 Region 大小为 1 MB,每个 Region 都会对应 2048 个 Card Page。

那么查找一个对象所在的 CardPage 只须要简略的计算就能够得出:

介绍完了 CardTable,上面说说 G1 中 RSet 和 CardTable 如何配合工作。

每个区域中都有一个 RSet,通过 hash 表实现,这个 hash 表的 key 是援用本区域的 其余区域 的地址,value 是一个数组,数组的元素是 援用方的对象 所对应的 Card Page 在 Card Table 中的下标。

如下图所示,区域 B 中的对象 b 援用了区域 A 中的对象 a,这个援用关系跨了两个区域。b 对象所在的 CardPage 为 122,在区域 A 的 RSet 中,以区域 B 的地址作为 key,b 对象所在 CardPage 下标为 value 记录了这个援用关系,这样就实现了这个跨区域援用的记录。

不过这个 CardTable 的粒度有点粗,毕竟一个 CardPage 有 512B,在一个 CardPage 内可能会存在多个对象。所以在扫描标记时,须要扫描 RSet 中关联的整个 CardPage。

写入屏障

写入屏障 (Write Barrier) 也是 GC 里的一个关键技术(不是 linux 里的 membarrier),当产生援用关系的更新时,通过写入屏障来(这里指转移用的写入屏障)记录这个援用关系的变更,只是一系列函数而已,就像这样(伪代码):

def evacuation_write_barrier(obj, field, newobj){
    // 查看援用和被援用新对象是否在同一个区域
    if(!check_cross_ref(obj, newobj)){return}
    // 不反复增加 dirty_card
    if(is_dirty_card(obj)){return}
    to_dirty(obj);
    // 将 obj 增加到 newobj 所在 region 的 rs
    add_to_rs(obj, newobj);
}

为了便于了解,下面的伪代码屏蔽了一些细节,理解外围工作内容即可

不过在 G1 里,不止一种写入屏障,像后面介绍的 SATB 也是有的写入屏障,这里不做过多介绍

分代回收

G1 中有两种回收模式:

  1. 齐全年老代 GC(fully-young collection),也称年老代垃圾回收(Young GC)
  2. 局部年老代 GC(partially-young collection)又称混合垃圾回收(Mixed GC)

齐全年老代 GC 是只抉择年老代区域(Eden/Survivor)进入回收汇合(Collection Set,简称 CSet)进行回收的模式。年老代 GC 的过程和其余的分代回收器差不多,新创建的对象调配至 Eden 区域,而后将标记存活的对象挪动至 Survivor 区,达到降职年龄的就降职到老年代区域,而后清空原区域(不过这里可没有年老代复制算法中两个 Survivor 的替换过程)。

年老代 GC 会抉择 所有的年老代区域 退出回收汇合中,然而为了满足用户进展工夫的配置,在每次 GC 后会调整这个最大年老代区域的数量,每次回收的区域数量可能是变动的

上面是一个齐全年老代 GC 过程的简略示意图:将抉择的年老代区域中所有存活的对象,挪动至 Survivor 区域,而后清空原区域

下面只是一个繁难的回收过程示意,接下来具体介绍年老代的回收过程

年老代垃圾回收(齐全年老代 GC)

当 JVM 无奈将新对象调配到 eden 区域时,会触发年老代的垃圾回收(年老代垃圾回收是齐全暂停的,尽管局部过程是并行,但暂停和并行并不抵触)。也会称为“evacuation pause”

步骤 1. 抉择收集汇合(Choose CSet),G1 会在遵循用户设置的 GC 暂停工夫下限的根底上,抉择一个 最大年老带区域数,将这个数量的所有年老代区域作为收集汇合。

如下图所示,此时 A /B/ C 三个年老代区域都曾经作为收集汇合,区域 A 中的 A 对象和区域 B 中的 E 对象,被 ROOTS 间接援用(图上为了简略,将 RS 间接援用到对象,实际上 RS 援用的是对象所在的 CardPage)

步骤 2. 根解决(Root Scanning),接下来,须要从 GC ROOTS 遍历,查找从 ROOTS直达到收集汇合的对象,挪动他们到 Survivor 区域的同时将他们的援用对象退出标记栈

如下图所示,在根解决阶段,被 GC ROOTS 间接援用的 A / E 两个对象间接被复制到了 Survivor 区域 M,同时 A / E 两个对象所 援用路线上的所有对象,都被退出了标记栈(Mark Stack),这里包含 E ->C->F,这个 F 对象也会被退出标记栈中

步骤 3. RSet 扫描(Scan RS),将 RSet 作为 ROOTS 遍历,查找可直达到收集汇合的对象,挪动他们到 Survivor 区域的同时将他们的援用对象退出标记栈

在 RSet 扫描之前,还有一步更新 RSet(Update RS)的步骤,因为 RSet 是先写日志,再通过一个 Refine 线程进行解决日志来保护 RSet 数据的,这里的更新 RSet 就是为了保障 RSet 日志被解决实现,RSet 数据残缺才能够进行扫描

如下图所示,老年代区域 C 中援用年老代 A 的这个援用关系,被记录在年老代的 RSet 中,此时遍历这个 RS,将老年代 C 区域中 D 对象援用的年老代 A 中的 B 对象,增加到标记栈中

步骤 4. 挪动(Evacuation/Object Copy),遍历下面的标记栈,将栈内的所有所有的对象挪动至 Survivor 区域(其实说是挪动,实质上还是复制)

如下图所示,标记栈中记录的 C /F/ B 对象被挪动到 Survivor 区域中    

当对象年龄超过降职的阈值时,对象会间接挪动到老年代区域,而不是 Survivor 区域。

对象挪动后,须要更新援用的指针,这块具体的做法能够参考我的另一篇文章《垃圾回收算法实现之 – 复制(残缺可运行 C 语言代码)》

收尾步骤. 剩下的就是一些收尾工作,Redirty(配合上面的并发标记)Clear CT(清理 Card Table),Free CSet(清理回收汇合),清空挪动前的区域增加到闲暇区等等,这些操作个别耗时都很短

混合回收(局部年老代 GC)

混合回收,也称局部年老代 GC,会抉择所有年老代区域(Eden/Survivor)(最大年老代分区数)和局部老年代区域进去回收汇合进行回收的模式。年老代区域对象挪动到 Survivor 区,老年代区域挪动到老年代区域。因为 G1 中老年代区域的回收形式和新生代一样是“移动式”,被回收区域在挪动后会全副清空,所以不会像其余应用革除算法的回收器一样(比方 CMS)有碎片问题。

上面是一个局部年老代 GC 过程的简略示意图:

混合回收的执行过程次要蕴含两步:

  1. 并发标记(concurrent marking)– 增量式并发的标记存活对象,标记过程中 Mutator 的援用更新也会被标记
  2. 挪动 / 转移(evacuation)- 和年老代的挪动过程统一,复用代码,最大的不同是将并发标记的后果也进行解决

并发标记

并发标记的目标是标记存活对象,为挪动过程做筹备。在并发标记的过程中,存活对象的标记和 Mutator 的运行是并发进行的。所以这个标记过程中援用变动的更新,是并发标记过程中最简单的局部。

G1 的并发标记设计,是基于 CMS 回收器的,所以整体标记过程和 CMS 中的并发标记很像。并发标记会对区域内所有的存活对象进行标记,那么未标记的对象就是垃圾须要回收(这里“并发”的目标是为了升高 Mutator 的暂停工夫)

当老年代应用的内存加上本次行将调配的内存占到总内存的 45%,就会启动混合回收,进行并发标记。

并发标记并不是间接在对象上进行标记,而是用了一个独立的数据容器 – 标记位图(MarkBitMap),对立的对区域中的所有对象进行标记,在挪动时通过这个位图就能够判断对象是否存活

标记位图

每个区域内都有两个标记位图(Mark Bitmap):next 和 prev。next 是本次标记的标记位图,而 prev 是上次标记的标记位图,保留上次标记的后果。

标记位图就是通过对象地址,映射到标记位图中的数据位,标记位图中的每一个 bit 都代表一个对象的标记状态,如下图所示:

每个区域中,还有 4 个标记地位的指针,别离是 bottom,top,nextTAMS,prevTAMS。因为是并发标记,标记的同时 Mutator 会调配对象、批改援用关系,而且并发标记(这里指的是并发 标记子阶段)会被年老代 GC 所中断,中断后持续就须要基于上次中断的点持续标记,所以这几个指针能够了解为记录变动点,有点游戏里暂存点的意思。

如下图所示,某个区域在进行标记前。bottom 代表这个区域的底部,top 示意区域内存的顶部(即使用量),TAMS(Top-at-Mark-Start,标记开始时的 top),prevTAMS 和 nextTAMS 即上 / 下一次的标记信息

在标记时,如果该区域又调配了一些对象,那么 top 指针就会挪动,那么此时 top-nextTAMS 就是标记过程中的新对象

并发标记的过程分为以下几个步骤:

初始标记(Initial Mark)

标记由根间接援用的对象(STW),这个过程是在年老代 GC 中实现的,不过不是每次年老代 GC 都会进行初始标记。

并发标记(Concurrent Mark)

以步骤 1 的标记后果作为 root,遍历可达的对象进行标记,和 mutator 并行,并且可被年老代 GC 中断,年老代 GC 实现后可持续进行标记

SATB

SATB (Snapshot At The Beginning,初始快照),是一种将并发标记阶段开始时对象间的援用关系,以逻辑快照的模式进行保留的伎俩

这个解释有点……形象,简略了解就是,在并发标记时,以以后的援用关系作为根底援用数据,不思考 Mutator 并发运行时对援用关系的批改(Snapshot 命名的由来),标记时是存活状态就认为是存活状态,同时利用 SATB Write Barrier 记录援用变动。具体的 SATB 解释,能够参考我的另一篇文章《SATB 的一些了解》

最终标记(Remark)

标记脱漏的对象,次要是 SATB 相干 **(STW)

清理(Cleanup)

计算标记区域的流动对象数量,清理没有存活对象的区域(标记后没有存活对象,并不是正经的回收阶段),对区域排序等(局部 STW)

混合收集

这里的混合收集,是指混合回收 GC 下的回收过程。在并发标记实现后,就能够进行混合收集了(mixed),混合收集阶段和年老代 GC 统一,从并发标记的后果 /ROOTS/RSet 遍历回收存活对象即可,只是多了老年代区域的回收。

Full GC

当混合回收无奈跟上内存调配的速度,导致老年代也满了,就会进行 Full GC 对整个堆进行回收。G1 中的 Full GC 也而是单线程串行的,而且是全暂停,应用的是标记 - 整顿算法,代价十分高。

暂停工夫的管制

G1 在挪动过程中尽管也是全暂停,不过 G1 在抉择回收汇合上是变动的,每次只抉择局部的区域进行回收,通过计算每个区域的预测暂停工夫来保障每次回收所占用的工夫。简略的说就是将一次残缺的 GC 拆分成屡次短时间的 GC 从而升高暂停的工夫,尽量保障每次的暂停工夫在用户的配置范畴(-XX:MaxGCPauseMilli)内。

年老代大小的配置

G1 为了管制暂停工夫,年老代最大区域数是动静调整的,不过如果手动设置了年老代大小,比方 Xmn/MaxNewSize/NewRatio 等,并且年老代最大和最小值一样,那么相当于禁用了这个最大区域数调整的性能,可能会导致暂停工夫管制的生效(因为年老代 GC 是抉择全副区域的,区域过多会导致暂停工夫的减少)。

所以 G1 中尽量不要设置年老代的大小,让 G1 主动的进行调整

日志解读

年老代 GC 日志(齐全年老代)

//[GC pause (G1 Evacuation Pause) (young) 代表齐全年老代回收
// 0.0182341 secs 是本次 GC 的暂停工夫
0.184: [GC pause (G1 Evacuation Pause) (young), 0.0182341 secs 是本次 GC 的暂停工夫]
// 并行 GC 线程,一共有 8 个
   [Parallel Time: 16.7 ms, GC Workers: 8]
      /* 这一行信息阐明的是这 8 个线程开始的工夫,Min 示意最早开始的线程工夫,Avg 示意均匀开始工夫,Max 示意的是最晚开始工夫,Diff 为最早和最晚的时间差。这个值越大阐明线程启动工夫越不平衡。线程启动的工夫依赖于 GC 进入平安点的状况。对于平安点能够参考后文的介绍。*/
      [GC Worker Start (ms):  184.2  184.2  184.2  184.3  184.3  184.4  186.1  186.1
       Min: 184.2, Avg: 184.7, Max: 186.1, Diff: 1.9]
      /* 根解决的工夫,这个工夫蕴含了所有强根的工夫,分为 Java 根,别离为 Thread、JNI、CLDG;和 JVM 根上面的 StringTable、Universe、JNI Handles、ObjectSynchronizer、FlatProfiler、Management、SystemDictionary、JVMTI */
      [Ext Root Scanning (ms):  0.3  0.2  0.2  0.1  0.1  0.0  0.0  0.0
       Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.8]
         /*Java 线程解决工夫,次要是线程栈。这个工夫蕴含了根间接援用对象的复制工夫,如果根超级大,这个工夫可能会减少 */
         [Thread Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
         [StringTable Roots (ms):  0.0  0.1  0.1  0.1  0.1  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
         [Universe Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [JNI Handles Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [ObjectSynchronizer Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [FlatProfiler Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Management Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [SystemDictionary Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [CLDG Roots (ms):  0.3  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.3]
         [JVMTI Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
       // CodeCache Roots 实际上是在解决 Rset 的时候的统计值,它蕴含上面的
       // UpdateRS,ScanRS 和 Code Root Scanning
         [CodeCache Roots (ms):  5.0  3.9  2.2  3.3  2.1  2.2  0.6  2.2
          Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.6]
         [CM RefProcessor Roots (ms):  0.0
         0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Wait For Strong CLD (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Weak CLD Roots (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [SATB Filtering (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
       // 这个就是 GC 线程更新 RSet 的工夫破费,留神这里的工夫和咱们在 Refine 外面解决 RSet
       // 的工夫没有关系,因为它们是不同的线程解决
       [Update RS (ms):  5.0  3.9  2.2  3.3  2.1  2.2  0.6  2.2
        Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.5]
          // 这里就是 GC 线程解决的白区中的 dcq 个数
         [Processed Buffers:  8  8  7  8  8  7  2  4
          Min: 2, Avg: 6.5, Max: 8, Diff: 6, Sum: 52]
      // 扫描 RSet 找到被援用的对象
      [Scan RS (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
       Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms):  0.0  0.0  0.0  0.0  0.0  0.1  0.0  0.0
       Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
      // 这个就是所有活着的对象(除了强根间接援用的对象,在 Java 根解决时会间接复制)复制
      // 到新的分区破费的工夫。从这里也能够看出复制基本上是最破费工夫的操作。[Object Copy (ms):  11.3  12.5  14.2  13.1  14.3  14.2  14.2  12.5
       Min: 11.3, Avg: 13.3, Max: 14.3, Diff: 3.0, Sum: 106.3]
      // GC 线程完结的工夫信息。[Termination (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
       Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts:  1  1  1  1  1  1  1  1
          Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
      // 这个是并行处理时其余解决所破费的工夫,通常是因为 JVM 析构开释资源等
      [GC Worker Other (ms):  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
       Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      // 并行 GC 破费的总体工夫
      [GC Worker Total (ms):  16.6  16.6  16.6  16.5  16.5  16.4  14.7  14.7
       Min: 14.7, Avg: 16.1, Max: 16.6, Diff: 1.9, Sum: 128.7]
      // GC 线程完结的工夫信息
      [GC Worker End (ms):  200.8  200.8  200.8  200.8  200.8  200.8  200.8  200.8
       Min: 200.8, Avg: 200.8, Max: 200.8, Diff: 0.0]
    // 上面是其余工作局部。// 代码扫描属于并行执行局部,蕴含了代码的调整和回收工夫
    [Code Root Fixup: 0.0 ms]   
    [Code Root Purge: 0.0 ms]
    // 革除卡表的工夫
    [Clear CT: 0.1 ms]
    [Other: 1.5 ms]
      // 抉择 CSet 的工夫,YGC 通常是 0
      [Choose CSet: 0.0 ms]
      // 援用解决的工夫,这个工夫是发现哪些援用对象能够革除,这个是能够并行处理的
      [Ref Proc: 1.1 ms]
      // 援用从新激活
      [Ref Enq: 0.2 ms]
      // 重构 RSet 破费的工夫
      [Redirty Cards: 0.1 ms]
         [Parallel Redirty:  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0
          Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
         [Redirtied Cards:  8118  7583  6892  4496  0  0  0  0
          Min: 0, Avg: 3386.1, Max: 8118, Diff: 8118, Sum: 27089]
          // 这个信息是是能够并行处理的,这里是线程重构 RSet 的数目
       // 大对象解决工夫
      [Humongous Register: 0.0 ms]
         [Humongous Total: 2]
          // 这里阐明有 2 个大对象
         [Humongous Candidate: 0]
          // 可回收的大对象 0 个
      // 如果有大对象要回收,回收破费的工夫,回收的个数
      [Humongous Reclaim: 0.0 ms]
         [Humongous Reclaimed: 0]
      // 开释 CSet 中的分区破费的工夫,有新生代的信息和老生代的信息。[Free CSet: 0.0 ms]
         [Young Free CSet: 0.0 ms]
         [Non-Young Free CSet: 0.0 ms]
    // GC 完结后 Eden 从 15M 变成 0,下一次应用的空间为 21M,S 从 2M 变成 3M,整个堆从
    // 23.7M 变成 20M
    [Eden: 15.0M(15.0M)->0.0B(21.0M) Survivors: 2048.0K->3072.0K 
     Heap: 23.7M(256.0M)->20.0M(256.0M)]

老年代垃圾回收(局部年老代 / 混合回收)日志

并发标记日志

并发标记是全局的,和回收过程是两个阶段,所以并发标记能够说是独立的。

// 并发标记 - 初始标记阶段,在年老代 GC 中实现
100.070: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0751469 secs]
  [Parallel Time: 74.7 ms, GC Workers: 8]
    [GC Worker Start (ms): Min: 100070.4, Avg: 100070.5, Max: 100070.6, Diff: 
      0.1]
    [Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 
      1.6]
    [Update RS (ms): Min: 0.6, Avg: 1.1, Max: 1.5, Diff: 0.9, Sum: 8.9]
       [Processed Buffers: Min: 1, Avg: 1.6, Max: 4, Diff: 3, Sum: 13]
    [Scan RS (ms): Min: 1.0, Avg: 1.4, Max: 1.9, Diff: 0.9, Sum: 10.8]
    [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 
      0.0]
    [Object Copy (ms): Min: 71.5, Avg: 71.5, Max: 71.6, Diff: 0.1, Sum: 572.1]
    [Termination (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.6]
       [Termination Attempts: Min: 1382, Avg: 1515.5, Max: 1609, Diff: 227, 
         Sum: 12124]
    [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
    [GC Worker Total (ms): Min: 74.5, Avg: 74.5, Max: 74.6, Diff: 0.1, Sum: 
      596.3]
    [GC Worker End (ms): Min: 100145.1, Avg: 100145.1, Max: 100145.1, Diff: 
      0.0]
  [Code Root Fixup: 0.0 ms]
  [Code Root Purge: 0.0 ms]
  [Clear CT: 0.1 ms]
  [Other: 0.4 ms]
    [Choose CSet: 0.0 ms]
    [Ref Proc: 0.1 ms]
    [Ref Enq: 0.0 ms]
    [Redirty Cards: 0.1 ms]
    [Humongous Register: 0.0 ms]
    [Humongous Reclaim: 0.0 ms]
    [Free CSet: 0.0 ms]
  [Eden: 23.0M(23.0M)->0.0B(14.0M) Survivors: 4096.0K->4096.0K Heap: 84.5M
    (128.0M)->86.5M(128.0M)]
[Times: user=0.63 sys=0.00, real=0.08 secs]

// 把 YHR 中 Survivor 分区作为根,开始并发标记根扫描
100.146: [GC concurrent-root-region-scan-start]
// 并发标记根扫描完结,破费了 0.0196297,留神扫描和 Mutator 是并发进行,同时有多个线程并行
100.165: [GC concurrent-root-region-scan-end, 0.0196297 secs]
// 开始并发标记子阶段,这里从所有的根援用:包含 Survivor 和强根如栈等登程,对整个堆进行标记
100.165: [GC concurrent-mark-start]
// 标记完结,破费 0.08848s
100.254: [GC concurrent-mark-end, 0.0884800 secs]
// 这里是再标记子阶段,包含再标记、援用解决、类卸载解决信息
100.254: [GC remark 100.254: [Finalize Marking, 0.0002228 secs] 100.254: 
  [GC ref-proc, 0.0001515 secs] 100.254: [Unloading, 0.0004694 secs], 
  0.0011610 secs]
  [Times: user=0.00 sys=0.00, real=0.00 secs]
// 革除解决,这里的革除仅仅回收整个分区中的垃圾
// 这里还会调整 RSet,以加重后续 GC 中 RSet 根的解决工夫
100.255: [GC cleanup 86M->86M(128M), 0.0005376 secs]
  [Times: user=0.00 sys=0.00, real=0.00 secs]

混合回收日志

// 混合回收 Mixed GC 其实和 YGC 的日志相似,能看到 GC pause(G1EvacuationPause)(mixed)这样的信息
// 日志剖析参考 Y 年老代 GC。122.132: [GC pause (G1 Evacuation Pause) (mixed), 0.0106092 secs]
  [Parallel Time: 9.8 ms, GC Workers: 8]
    [GC Worker Start (ms): Min: 122131.9, Avg: 122132.0, Max: 122132.0, 
      Diff: 0.1]
    [Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
    [Update RS (ms): Min: 0.5, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.4]
      [Processed Buffers: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 14]
    [Scan RS (ms): Min: 1.0, Avg: 1.3, Max: 1.5, Diff: 0.5, Sum: 10.4]
    [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 
      0.0]
    [Object Copy (ms): Min: 7.5, Avg: 7.6, Max: 7.7, Diff: 0.2, Sum: 60.9]
    [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [Termination Attempts: Min: 92, Avg: 105.1, Max: 121, Diff: 29, Sum: 841]
    [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
    [GC Worker Total (ms): Min: 9.7, Avg: 9.7, Max: 9.8, Diff: 0.1, Sum: 77.6]
    [GC Worker End (ms): Min: 122141.7, Avg: 122141.7, Max: 122141.7, Diff: 0.0]
  [Code Root Fixup: 0.0 ms]
  [Code Root Purge: 0.0 ms]
  [Clear CT: 0.2 ms]
  [Other: 0.7 ms]
    [Choose CSet: 0.0 ms]
    [Ref Proc: 0.1 ms]
    [Ref Enq: 0.0 ms]
    [Redirty Cards: 0.5 ms]
    [Humongous Register: 0.0 ms]
    [Humongous Reclaim: 0.0 ms]
    [Free CSet: 0.0 ms]
  [Eden: 3072.0K(3072.0K)->0.0B(5120.0K) Survivors: 3072.0K->1024.0K 
    Heap: 105.5M(128.0M)->104.0M(128.0M)]
[Times: user=0.00 sys=0.00, real=0.01 secs]

罕用参数

这里只列出了一些最根底的参数。大多数状况下的 GC 调优,调的只是一些内存 / 比例 / 工夫,对各种线程数调整的场景很少,默认参数下也足够了。

# 启动 G1
-XX:+UseG1GC

# 最小堆内存
-Xms8G

# 最大堆内存
-Xmx8G

# metaspace 初始值
-XX:MetaspaceSize=256M

# 冀望的最大暂停工夫,默认 200ms
-XX:MaxGCPauseMillis

# 简称为 IHOP,默认值为 45,这个值是启动并发标记的阈值,当老年代应用内存占用堆内存的 45% 启动并发标记。# 如果该过大,可能会导致 mixed gc 跟不上内存调配的速度从而导致 full gc
-XX:InitiatingHeapOccupancyPercent

# G1 主动调整 IHOP 的指,JDK9 之后可用
-XX:+G1UseAdaptiveIHOP

# 并发标记时能够卸载 Class,这个操作比拟耗时,对 Perm/MetaSpace 进行清理,默认未开启
-XX:+ClassUnloadingWithConcurrentMark

# 多个线程并行执行 java.lang.Ref.*,对象回收前的援用解决
-XX:-ParallelRefProcEnabled

和其余回收器的比照

  • 和 Parallel GC 相比,G1 的增量并行回收暂停工夫更短
  • 和 CMS 相比,G1 因为采纳移动式算法,所以没有碎片问题,更无效的利用了内存

总结

G1 的全称是 Garbage First,意思是“垃圾优先”。什么叫垃圾优先呢?

在并发标记时,会依据存活对象的数量 / 大小,对标记的区域进行降序排序。到了挪动过程时,就会优先选择 挪动效率高(垃圾多,存活对象少,须要挪动的就少)的区域作为回收汇合,这就是 Garbage First 命名的由来

但 G1 并不属于一个高效率的回收器,对老年代应用移动式的回收算法,尽管没有碎片问题,但效率是较低的。因为老年代对象大多数是存活的,所以每次回收须要挪动的对象很多。而革除算法中是革除死亡的对象,所以从效率上来看,革除算法在老年代中会更好。

然而因为 G1 这个可管制暂停的增量回收,能够保障每次暂停工夫在容许范畴内,对于大多数利用来说,暂停工夫比吞吐量更重要。再加上 G1 的各种细节优化,效率曾经很高了。

垃圾回收系列贴(残缺可运行 C 语言代码)

  • 垃圾回收算法实现之 – 标记 - 革除(残缺可运行 C 语言代码)
  • 垃圾回收算法实现之 – 援用计数(残缺可运行 C 语言代码)
  • 垃圾回收算法实现之 – 复制(残缺可运行 C 语言代码)
  • 垃圾回收算法实现之 – 标记 - 整顿(残缺可运行 C 语言代码)
  • 垃圾回收算法实现之 – 分代回收(残缺可运行 C 语言代码)

参考 & 材料

  • 《深刻 Java 虚拟机:JVM G1GC 的算法与实现》– 中村成洋 (作者) 吴炎昌 , 杨文轩 (译者)
  • 《垃圾回收的算法与实现》- 中村成洋 , 相川光 , 竹内郁雄 (作者) 丁灵 (译者)
  • 《JVM G1 源码剖析和调优》- 彭成寒 著
  • 《Java Performance Companion》- Charlie Hunt, Monica Beckwith, Poonam Parhar, Bengt Rutisson
  • Garbage-First Garbage Collector – Oracle
  • Getting Started with the G1 Garbage Collector – Oracle
  • Understanding the JDK’s New Superfast Garbage Collectors
  • G1: One Garbage Collector To Rule Them All – InfoQ
  • GC Algorithms: Implementations – Plumbr
  • JVM Garbage Collectors Benchmarks Report 19.12
  • https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector
  • [[HotSpot VM] 求教 G1 算法的原理 – R 大](https://hllvm-group.iteye.com…
  • [[HotSpot VM] 对于 incremental update 与 SATB 的一点了解 – R 大](https://hllvm-group.iteye.com…
  • 本人对于 VM 的帖的目录 – ITEYE – R 大

原创不易,转载请在结尾驰名文章起源和作者。如果我的文章对您有帮忙,请点赞珍藏激励反对。原文链接 https://segmentfault.com/a/1190000039411521/edit

退出移动版