《Java 虚拟机原理》5.3 G1 原理剖析及日志分析

1.G1 介绍

G1 与 GMS 垃圾收集器的区别
① CMS 在老年代产生了内存碎片,导致对象的内存分配是采用了空闲链表的方式。G1 回收的内存是连续的,所以内存分配的方式是碰撞指针
② G1 和 GMS 的内存模式是完全不一样的。G1 是以 Region 为单位进行内存回收,即将内存划分成一个个固定大小的 Region,每个 Region 属于Eden、Survivor、Old 或者 Humongous。GMS 采用传统的内存划分,将整个 Heap 分为 Eden、Survivor 和 Old。

1.1 G1 的特点

● 内存不再固定划分新生代和老年代,而是使用分区 Region 对于内存进行分块。Region 可能属于新生代或者老年代,同时分配给新生代还是老年代是由 G1 控制的。
● 通过垃圾收集的预期停顿时间,根据 Region 的大小和回收价值进行最有效率的回收。

1.2 G1 基础知识

(1)内存模型 Region

G1取消了固定分代的概念,取而代之的是使用分区 Region 概念,把内存切分成一个个小块,同时各个小块又分为新生代(eden、Survior区)、老年代、大对象(humongous区),其结构如下:

image.png

每个 Region 被标记了 E、S、O 和 H,说明每个 Region 在运行时都充当了一种角色。H 它代表 Humongous,其存储的是巨型对象。当新建对象大小超过 Region 大小一半时,直接在新的一个或多个连续 Region 中分配,并标记为 H。

(2)跨代引用

跨代引用的问题是当进行 Young GC 时,Young 区的对象可能还存在 Old 区的引用。G1 引入 Card Table 简称 CardRemember Set 简称 RSet,采用空间换时间的基本思想
RSet 用于记录外部指向本 Region 的所有引用,且每个 Region 维护一个 RSet。JVM 将每个 Region 分为固定大小的 Card(类似内存分页)。如下图所示,绿色部分的 Card 中有对象引用了其它 Region Card 中的对象,采用蓝色实线箭头表示。RSet 实质是 HashTable,Key 是 Region 的起始地址,Value 是 Card(字节数组)。

image.png

(3)SATB

SATB 的作用是保证了在并发标记过程中新分配对象不会漏标

SATB 的全称是 Snapshot At The Beginning,即 GC 开始时保存的对象快照,主要用于垃圾收集的并发标记阶段,解决了 CMS 垃圾收集器重新标记阶段长时间Stop The Word 的潜在风险。
如下图所示,Region 包含了 5 个指针,分别是 bottom、previous TAMS、next TAMS、top 和 end,其中 previous TAMS、next TAMS 是前后两次发生并发标记时的位置,全称 top-at-mark-start

image.png

1.3 G1 的 GC 模式

(1)Young GC

Young GC 是回收所有年轻代的 Eden 和 Survivor Region。触发条件是不能在 Eden Region 分配新的对象

Young GC 前后.png

(2)Mixed GC

Mixed GC 是回收所有的年轻代的 Region + 部分老年代的 Region

问题1:为什么是老年代的部分 Region?
回收部分老年代的相关参数是 -XX:MaxGCPauseMillis 表示G1 收集过程目标停顿时间,默认值 200ms。G1 采用停顿预测模型(Pause Prediction Model),在尽量满足停顿时间的条件下,挑选部分老年代的 Region 继续回收。
问题2:什么时候触发 Mixed GC?
Mixed GC 触发的参数,例如 -XX:InitiatingHeapOccupancyPercent 表示老年代占整个堆大小的百分比,默认值是 45%。如果达到该阈值,就会触发一次 Mixed GC。

全局并发标记 global concurrent marking
初始标记(Initial Mark,STW),表示了从 GC Root 开始直接可达的对象。初始标记阶段借用 Young GC的暂停。
并发标记(Concurrent Marking),表示从 GC Root 开始对 Heap 中的对象标记,标记线程与应用程序线程并行执行,并且收集各个 Region 的存活对象信息。
最终标记(Remark,STW),表示那些在并发标记阶段发生变化的对象,将被回收。
清除垃圾(Cleanup,部分STW),表示如果发现完全没有活对象的 Region 就会将其整体回收到可分配 Region 列表中,并清除空 Region。

拷贝存活对象 Evacuation
Evacuation 阶段是 Stop The Word 的,负责把一部分 Region 里的活对象并行拷贝到空 Region,然后回收原本的 Region。Evacuation 阶段可以自由选择任意多个Region 来构成收集集合 Collection Set 即 CSet

Mixed GC 前后.png

(3)Full GC

当 Mixed GC 的速度赶不上应用程序申请内存的速度的时候,Mixed GC 就会降级到 Full GC,使用 Serial GC。Full GC 会导致长时间 Stop The Word。

2.G1 GC 日志分析

2.1 新生代 young GC 日志

参考
1.《Orcale 官方文章 Getting Started with the G1 Garbage Collector》
2.《G1 – Garbage First》

设置 GC 日志的 JVM 参数 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC,GC 日志详情如下所示。

2021-03-27T21:07:37.834+0000: 42873.305: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0330341 secs]   
// G1 仅暂停清理(年轻)区域。 暂停在 JVM 启动后 2021-03-27T21:07:37.834 毫秒开始,暂停持续时间以挂钟时间测量为 0.0330341 秒。
   [Parallel Time: 27.8 ms, GC Workers: 2]
   // 实际耗时 27.8 毫秒,下列活动由 2 个线程并行执行
      [GC Worker Start (ms): Min: 42873306.2, Avg: 42873306.2, Max: 42873306.2, Diff: 0.0]
      [Ext Root Scanning (ms): Min: 14.3, Avg: 19.2, Max: 24.2, Diff: 10.0, Sum: 38.5]
      [Update RS (ms): Min: 0.0, Avg: 1.6, Max: 3.2, Diff: 3.2, Sum: 3.2]
         [Processed Buffers: Min: 0, Avg: 16.0, Max: 32, Diff: 32, Sum: 32]
      [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.4, Max: 0.8, Diff: 0.8, Sum: 0.8]
      [Object Copy (ms): Min: 2.6, Avg: 5.5, Max: 8.5, Diff: 5.9, Sum: 11.0]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 26.8, Avg: 26.8, Max: 26.8, Diff: 0.0, Sum: 53.6]
      [GC Worker End (ms): Min: 42873333.0, Avg: 42873333.0, Max: 42873333.0, Diff: 0.0]
   [Code Root Fixup: 0.1 ms]
   // 按照顺序进行释放用于管理并行活动的数据结构,耗时 0.1 ms。
   [Code Root Purge: 0.0 ms]
   // 按照顺序进行清理更多的数据结构,耗时 0 ms
   [Clear CT: 0.1 ms]
   [Other: 5.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 4.5 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.1 ms]
   [Eden: 62.0M(62.0M)->0.0B(61.0M) Survivors: 8192.0K->9216.0K Heap: 1258.0M(1408.0M)->1209.7M(1408.0M)]
 [Times: user=0.06 sys=0.00, real=0.03 secs] 
  // Eden 、Survivors 和 Heap 总内存的暂停前后使用量和容量
2021-03-27T21:07:37.868+0000: 42873.338: [GC concurrent-root-region-scan-start]
2021-03-27T21:07:37.876+0000: 42873.347: [GC concurrent-root-region-scan-end, 0.0083426 secs]
// GCRoots 扫描阶段
2021-03-27T21:07:37.876+0000: 42873.347: [GC concurrent-mark-start]
2021-03-27T21:07:38.254+0000: 42873.725: [GC concurrent-mark-end, 0.3780598 secs]
// GC 标记阶段
2021-03-27T21:07:38.254+0000: 42873.725: [GC remark 2021-03-27T21:07:38.254+0000: 42873.725: [Finalize Marking, 0.0001809 secs] 2021-03-27T21:07:38.254+0000: 42873.725: [GC ref-proc, 0.0087798 secs] 2021-03-27T21:07:38.263+0000: 42873.734: [Unloading, 0.1158455 secs], 0.1294324 secs]
 [Times: user=0.17 sys=0.00, real=0.13 secs] 
// stop the word,G1 会短暂停止应用程序线程以停止并发更新日志的流入并处理剩余的少量日志,并标记在启动并发标记周期时仍处于活动状态的任何未标记对象
2021-03-27T21:07:38.384+0000: 42873.855: [GC cleanup 1247M->1243M(1408M), 0.0010662 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
2021-03-27T21:07:38.385+0000: 42873.856: [GC concurrent-cleanup-start]
2021-03-27T21:07:38.385+0000: 42873.856: [GC concurrent-cleanup-end, 0.0000160 secs]
// 内存回收
2021-03-18T07:08:23.683+0000: 13004.683: [GC pause (G1 Humongous Allocation) (young) , 0.0255329 secs]  // 巨型对象 young GC
   [Parallel Time: 21.5 ms, GC Workers: 1]    // 并行8个线程,耗时21.5ms
      [GC Worker Start (ms):  13004683.5]
      [Ext Root Scanning (ms):  14.3]   // 扫描 root 的线程耗时
      [Update RS (ms):  1.6]
         [Processed Buffers:  40]   // Processed Buffers就是记录引用变化的缓存空间
      [Scan RS (ms):  0.2]   // 扫描 RS
      [Code Root Scanning (ms):  0.2]   // 根扫描耗时
      [Object Copy (ms):  4.7]   // 对象拷贝
      [Termination (ms):  0.0]
         [Termination Attempts:  1]
      [GC Worker Other (ms):  0.0]
      [GC Worker Total (ms):  21.0]   // GC 线程耗时
      [GC Worker End (ms):  13004704.5]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.2 ms]   // 清空 CardTable 耗时,RS 是依赖 CardTable 记录区域存活对象的
   [Other: 3.9 ms]
      [Choose CSet: 0.0 ms]   // 选取 CSet
      [Ref Proc: 3.2 ms]   // 弱引用、软引用的处理耗时
      [Ref Enq: 0.0 ms]   // 弱引用、软引用的入队耗时
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.2 ms]    // 释放被回收区域的耗时
   [Eden: 373.0M(395.0M)->0.0B(380.0M) Survivors: 1024.0K->14.0M Heap: 1189.9M(1408.0M)->830.5M(1408.0M)]   // 显示了堆的大小变化, Eden 从 373.0M 下降到 0, Survivors 从 1024.0K 上升到 14.0M,Heap 内存从 1189.9M 下降到  830.5M
 [Times: user=0.03 sys=0.00, real=0.03 secs] 
2021-03-18T07:08:23.708+0000: 13004.709: [GC concurrent-root-region-scan-start]   // 根区域扫描
2021-03-18T07:08:23.713+0000: 13004.713: [GC concurrent-root-region-scan-end, 0.0048903 secs]   
2021-03-18T07:08:23.713+0000: 13004.713: [GC concurrent-mark-start]    // 并发标记
2021-03-18T07:08:23.872+0000: 13004.872: [GC concurrent-mark-end, 0.1585816 secs] 
2021-03-18T07:08:23.933+0000: 13004.933: [GC remark 2021-03-18T07:08:23.933+0000: 13004.933: [Finalize Marking, 0.0002291 secs] 2021-03-18T07:08:23.933+0000: 13004.934: [GC ref-proc, 0.0080423 secs] 2021-03-18T07:08:23.941+0000: 13004.942: [Unloading, 0.0703731 secs], 0.0852099 secs]
 [Times: user=0.09 sys=0.00, real=0.09 secs] 
2021-03-18T07:08:24.019+0000: 13005.019: [GC cleanup 851M->764M(1408M), 0.0027544 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
2021-03-18T07:08:24.022+0000: 13005.022: [GC concurrent-cleanup-start]   // 重新标记又叫最终标记
2021-03-18T07:08:24.022+0000: 13005.022: [GC concurrent-cleanup-end, 0.0000804 secs]

2.2 混合 mixed GC 日志

2021-03-18T07:03:33.457+0000: 12714.458: [GC pause (G1 Evacuation Pause) (mixed), 0.1126267 secs]   // mixed GC 
   [Parallel Time: 110.7 ms, GC Workers: 1]
      [GC Worker Start (ms):  12714457.7]
      [Ext Root Scanning (ms):  12.9]
      [Update RS (ms):  68.6]
         [Processed Buffers:  107]
      [Scan RS (ms):  0.2]
      [Code Root Scanning (ms):  6.0]
      [Object Copy (ms):  22.8]
      [Termination (ms):  0.0]
         [Termination Attempts:  1]
      [GC Worker Other (ms):  0.0]
      [GC Worker Total (ms):  110.7]
      [GC Worker End (ms):  12714568.4]
   [Code Root Fixup: 0.5 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 1.3 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.5 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.4 ms]
   [Eden: 37.0M(37.0M)->0.0B(69.0M) Survivors: 33.0M->1024.0K Heap: 982.0M(1408.0M)->904.5M(1408.0M)]
 [Times: user=0.07 sys=0.00, real=0.11 secs]

2.3 老年代 Full GC 日志

2021-03-19T02:08:52.178+0000: 81433.178: [Full GC (Allocation Failure)  1331M->1190M(1408M), 0.3951381 secs]   // Full GC 失败, 其原因是空间分配失败(Allocation Failure)
   [Eden: 0.0B(68.0M)->0.0B(70.0M) Survivors: 2048.0K->0.0B Heap: 1331.5M(1408.0M)->1190.0M(1408.0M)], [Metaspace: 595088K->593735K(1597440K)]   // 显示了堆的大小变化
 [Times: user=0.40 sys=0.00, real=0.39 secs]    // 表示 GC 总共花了 0.39 秒

Full GC (Allocation Failure) 的一般原因:老年代存活的对象太多,基本没有被回收。

3.G1 常用参数

参数 默认值 含义
-XX:+UseG1GC 启用G1 垃圾收集器
-XX:G1HeapRegionSize=n 默认值将根据 heap size 算出最优解,最小值为 1Mb,最大值为 32Mb 指定 Region 的大小
-XX:MaxGCPauseMillis=200 默认值为 200 毫秒 期望的最大暂停时间
-XX:NewRatio=n 默认值为 2 新生代与老生代(new/old generation)的大小比例
-XX:SurvivorRatio=n 默认值为 8% eden/survivor 空间大小的比例
-XX:MaxTenuringThreshold=n 默认值为 15 提升年老代的最大临界值
-XX:G1NewSizePercent=5 默认值为 5% 设置年轻代的最小空间占比
-XX:G1MaxNewSizePercent=60 默认值为 60% 设置年轻代的最大空间占比
-XX:G1ReservePercent 默认值为 10% 设置一定比例的保留空间,让其保持空闲状态
-XX:ParallelGCThreads=n 如果 CPU <= 8,则默认 n = CPU 数量。如果 CPU > 8,则 n = CPU 数量 * 5/8 + 3 设置 STW 阶段的并行 worker 线程数
-XX:ConcGCThreads=n 默认值为 ParallelGCThreads 的 1/4 设置并发标记的 GC 线程数
-XX:InitiatingHeapOccupancyPercent=45 默认值为整个 Java 堆的 45% 设置标记周期的触发阈值

4.G1 调优

避免设置年轻代的大小
防止使用 -Xmn、-XX:NewRatio 指定年轻代大小,如果指定固定的年轻代大小,则会覆盖最大暂停时间目标,得不偿失。

期望的最大暂停时间
权衡延迟与吞吐量。最大暂停时间设置较小,可能需要较大的 GC 开销,反之则只需较小的 GC 开销。

GC 日志中内存耗尽的信息

2021-03-18T03:31:46.969+0000: 7.969: [GC pause (G1 Evacuation Pause) (young), 0.0376195 secs]

或者

2021-03-18T07:03:34.443+0000: 12715.443: [GC pause (G1 Evacuation Pause) (mixed), 0.0141364 secs]

解决该问题的策略:
增加 -XX:G1ReservePercent,以增加保留的 “to-space” 大小,堆内存一般也相应需要增加。
降低 -XX:InitiatingHeapOccupancyPercent 来尽早触发标记周期。
适当加大 -XX:ConcGCThreads 选项的值,增加并发标记的线程数。

巨型对象的内存分配

背景:如果某个对象超过单个 Region 空间的一半,则会被 G1 视为巨型对象,例如大数组和字符串。巨型对象会直接分配到老年代的 Humongous 区。

2021-03-18T08:30:18.826+0000: 17919.826: [GC pause (G1 Humongous Allocation) (young), 0.0664334 secs]

如果在 GC 日志中,看到由 Humongous 分配触发的大量 GC,导致在老年代形成大量的内存碎片。需要增加 -XX:G1HeapRegionSize,即对象小于 Region 的 50%,让之前的大对象不再被直接分配到 Humongous 区,而是走常规的对象分配方式。

你可能感兴趣的:(《Java 虚拟机原理》5.3 G1 原理剖析及日志分析)