介绍
G1 GC, 全称 Garbage-First Garbage Collector, 通过 - XX:+UseG1GC 参数来启用. G1 收集器是工作在堆内不同分区上的收集器, 分区既可以是年轻代也可以是老年代, 同一个代的分区不需要连续. 并且每个代分区的数量是可以动态调整的. 为老年代设置分区的目的是老年代里有的分区垃圾多, 有的分区垃圾少, 这样在回收的时候可以专注于收集垃圾多的分区, 这也是 G1 名称的由来. 不过这个算法并不适合新生代垃圾收集, 因为新生代的垃圾收集算法是复制算法, 但是新生代也使用了分区机制主要是因为便于代大小的调整.
G1 GC 是设计用来取代 CMS 的, 同 CMS 相比 G1 有以下优势:
1, 可预测的停顿模型
2, 避免了 CMS 的垃圾碎片
3, 超大堆的表现更出色
G1 关键概念
Region
G1 里面的 Region 的概念不同于传统的垃圾回收算法中的分区的概念. G1 默认把堆内存分为 1024 个分区, 后续垃圾收集的单位都是以 Region 为单位的. Region 是实现 G1 算法的基础, 每个 Region 的大小相等, 通过 - XX:G1HeapRegionSize 参数可以设置 Region 的大小. 如下图所示:
图中的 E 代表是 Eden 区, S 代表 Survivor,O 代表 Old 区, H 代表 humongous 表示巨型对象 (大小大小 Region 空间一半的对象). 从图中可以看出各个区域逻辑上并不是连续的. 并且一个 Region 在某一个时刻是 Eden, 在另一个时刻就可能属于老年代. G1 在进行垃圾清理的时候就是将一个 Region 的对象拷贝到另外一个 Region 中.
SATB
SATB 的全称是 Snapchat-At-The_Beginning.SATB 是维持并发 GC 的一种手段. G1 并发的基础就是 SATB.SATB 可以理解成在 GC 开始之前对堆内存里的对象做一次快照, 此时活的对象就认为是活的, 从而形成一个对象图. 在 GC 收集的时候, 新生代的对象也认为是活的对象, 除此之外其他不可达的对象都认为是垃圾对象.
如何找到在 GC 的过程中分配的对象呢? 每个 region 记录着两个 top-at-mark-start(TAMS) 指针, 分别为 prevTAMS 和 nextTAMS. 在 TAMS 以上的对象就是新分配的, 因而被视为隐式 marked. 通过这种方式我们就找到了在 GC 过程中新分配的对象, 并把这些对象认为是活的对象.
解决了对象在 GC 过程中分配的问题, 那么在 GC 过程中引用发生变化的问题怎么解决呢, G1 给出的解决办法是通过 Write Barrier.Write Barrier 就是对引用字段进行赋值做了环切. 通过 Write Barrier 就可以了解到哪些引用对象发生了什么样的变化.
RSet
RSet 全称是 Remember Set, 每个 Region 中都有一个 RSet, 记录的是其他 Region 中的对象引用本 Region 对象的关系 (谁引用了我的对象).G1 里面还有另外一种数据结构就 Collection Set(CSet),CSet 记录的是 GC 要收集的 Region 的集合, CSet 里的 Region 可以是任意代的. 在 GC 的时候, 对于 old->young 和 old->old 的跨代对象引用, 只要扫描对应的 CSet 中的 RSet 即可.
停顿预测模型
G1 收集器突出表现出来的一点是通过一个停顿预测模型来根据用户配置的停顿时间来选择 CSet 的大小, 从而达到用户期待的应用程序暂停时间. 通过 - XX:MaxGCPauseMillis 参数来设置. 这一点有点类似于 ParallelScavenge 收集器. 关于停顿时间的设置并不是越短越好. 设置的时间越短意味着每次收集的 CSet 越小, 导致垃圾逐步积累变多, 最终不得不退化成 Serial GC; 停顿时间设置的过长, 那么会导致每次都会产生长时间的停顿, 影响了程序对外的响应时间.
#G1 回收的过程
G1 垃圾回收分为两个阶段:
1, 全局并发标记阶段 (Global Concurrent marking)
2, 拷贝存活对象阶段 (evacuation)
全局并发标记阶段
全局并发标记阶段是基于 SATB 的, 与 CMS 有些类似, 但是也有不同的地方, 主要的几个阶段如下:
初始标记: 该阶段会 STW. 扫描根集合, 将所有通过根集合直达的对象压入扫描栈, 等待后续的处理. 在 G1 中初始标记阶段是借助 Young GC 的暂停进行的, 不需要额外的暂停. 虽然加长了 Young GC 的暂停时间, 但是从总体上来说还是提高的 GC 的效率.
并发标记: 该阶段不需要 STW. 这个阶段不断的从扫描栈中取出对象进行扫描, 将扫描到的对象的字段再压入扫描栈中, 依次递归, 直到扫描栈为空, 也就是说 trace 了所有 GCRoot 直达的对象. 同时这个阶段还会扫描 SATB write barrier 所记录下的引用.
最终标记: 也叫 Remark, 这个阶段也是 STW 的. 这个阶段会处理在并发标记阶段 write barrier 记录下的引用, 同时进行弱引用的处理. 这个阶段与 CMS 的最大的区别是 CMS 在这个阶段会扫描整个根集合, Eden 也会作为根集合的一部分被扫描, 因此耗时可能会很长.
清理: 该阶段会 STW. 清点和重置标记状态. 这个阶段有点像 mark-sweep 中的 sweep 阶段, 这个阶段并不会实际上去做垃圾的收集, 只是去根据停顿模型来预测出 CSet, 等待 evacuation 阶段来回收.
拷贝存活对象阶段
Evacuation 阶段是全暂停的. 该阶段把一部分 Region 里的活对象拷贝到另一部分 Region 中, 从而实现垃圾的回收清理. Evacuation 阶段从第一阶段选出来的 Region 中筛选出任意多个 Region 作为垃圾收集的目标, 这些要收集的 Region 叫 CSet, 通过 RSet 实现.
筛选出 CSet 之后, G1 将并行的将这些 Region 里的存活对象拷贝到其他 Region 中, 这点类似于 ParalledScavenge 的拷贝过程, 整个过程是完全暂停的. 关于停顿时间的控制, 就是通过选择 CSet 的数量来达到控制时间长短的目标.
G1 的收集模式:
YoungGC: 收集年轻代里的 Region
MixGC: 年轻代的所有 Region + 全局并发标记阶段选出的收益高的 Region
无论是 YoungGC 还是 MixGC 都只是并发拷贝的阶段.
分代 G1 模式下选择 CSet 有两种子模式, 分别对应 YoungGC 和 mixedGC:
YoungGC:CSet 就是所有年轻代里面的 Region
MixedGC:CSet 是所有年轻代里的 Region 加上在全局并发标记阶段标记出来的收益高的 Region
G1 的运行过程是这样的, 会在 Young GC 和 Mix GC 之间不断的切换运行, 同时定期的做全局并发标记, 在实在赶不上回收速度的情况下使用 Full GC(Serial GC). 初始标记是搭在 YoungGC 上执行的, 在进行全局并发标记的时候不会做 Mix GC, 在做 Mix GC 的时候也不会启动初始标记阶段. 当 MixGC 赶不上对象产生的速度的时候就退化成 Full GC, 这一点是需要重点调优的地方.
G1 最佳实践
在使用 G1 垃圾收集器的时候遵循以下实践可以少走不少弯路:
不断调优暂停时间指标
通过 XX:MaxGCPauseMillis=x 可以设置启动应用程序暂停的时间, G1 在运行的时候会根据这个参数选择 CSet 来满足响应时间的设置. 一般情况下这个值设置到 100ms 或者 200ms 都是可以的 (不同情况下会不一样), 但如果设置成 50ms 就不太合理. 暂停时间设置的太短, 就会导致出现 G1 跟不上垃圾产生的速度. 最终退化成 Full GC. 所以对这个参数的调优是一个持续的过程, 逐步调整到最佳状态.
不要设置新生代和老年代的大小
G1 收集器在运行的时候会调整新生代和老年代的大小. 通过改变代的大小来调整对象晋升的速度以及晋升年龄, 从而达到我们为收集器设置的暂停时间目标. 设置了新生代大小相当于放弃了 G1 为我们做的自动调优. 我们需要做的只是设置整个堆内存的大小, 剩下的交给 G1 自己去分配各个代的大小.
关注 Evacuation Failure
Evacuation Failure 类似于 CMS 里面的晋升失败, 堆空间的垃圾太多导致无法完成 Region 之间的拷贝, 于是不得不退化成 Full GC 来做一次全局范围内的垃圾收集.
G1 常用参数
参数 / 默认值 含义
-XX:+UseG1GC | 使用 G1 垃圾收集器 |
-XX:MaxGCPauseMillis=200 | 设置期望达到的最大 GC 停顿时间指标(JVM 会尽力实现,但不保证达到) |
-XX:InitiatingHeapOccupancyPercent=45 | 启动并发 GC 周期时的堆内存占用百分比. G1 之类的垃圾收集器用它来触发并发 GC 周期, 基于整个堆的使用率, 而不只是某一代内存的使用比. 值为 0 则表示”一直执行 GC 循环”. 默认值为 45. |
-XX:NewRatio=n | 新生代与老生代 (new/old generation) 的大小比例(Ratio). 默认值为 2. |
-XX:SurvivorRatio=n | eden/survivor 空间大小的比例(Ratio). 默认值为 8. |
-XX:MaxTenuringThreshold=n | 提升年老代的最大临界值(tenuring threshold). 默认值为 15. |
-XX:ParallelGCThreads=n | 设置垃圾收集器在并行阶段使用的线程数, 默认值随 JVM 运行的平台不同而不同. |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量. 默认值随 JVM 运行的平台不同而不同. |
-XX:G1ReservePercent=n | 设置堆内存保留为假天花板的总量, 以降低提升失败的可能性. 默认值是 10. |
-XX:G1HeapRegionSize=n | 使用 G1 时 Java 堆会被分为大小统一的的区(region)。此参数可以指定每个 heap 区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb. |
G1 日志分析
- // 新生代 GC
- 2018-05-03T10:21:43.209-0800: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0035356 secs] // 初始标记, 耗时 0.0035 秒
- [Parallel Time: 2.4 ms, GC Workers: 8] // 并行 8 个线程, 耗时 2.4ms
- [GC Worker Start (ms): Min: 813.1, Avg: 813.7, Max: 813.9, Diff: 0.7]
- [Ext Root Scanning (ms): Min: 0.0, Avg: 1.1, Max: 1.5, Diff: 1.5, Sum: 9.1] // 每个扫描 root 的线程耗时
- [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] // 更新 RS 的耗时, G1 中每块区域都有一个 RS 与之对应, RS 记录了该区域被其他区域引用的对象. 回收时, 就把 RS 作为根集的一部分, 从而加快回收
- [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0] //Processed Buffers 就是记录引用变化的缓存空间
- [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] // 扫描 RS
- [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] // 根扫描耗时
- [Object Copy (ms): Min: 0.0, Avg: 0.5, Max: 1.3, Diff: 1.3, Sum: 3.6] // 对象拷贝
- [Termination (ms): Min: 0.0, Avg: 0.2, Max: 0.2, Diff: 0.2, Sum: 1.2]
- [Termination Attempts: Min: 1, Avg: 1.8, Max: 4, Diff: 3, Sum: 14]
- [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
- [GC Worker Total (ms): Min: 1.6, Avg: 1.8, Max: 2.3, Diff: 0.8, Sum: 14.1] //GC 线程耗时
- [GC Worker End (ms): Min: 815.4, Avg: 815.4, Max: 815.4, Diff: 0.0]
- [Code Root Fixup: 0.0 ms]
- [Code Root Purge: 0.0 ms]
- [Clear CT: 0.1 ms] // 清空 CardTable 耗时, RS 是依赖 CardTable 记录区域存活对象的
- [Other: 1.1 ms]
- [Choose CSet: 0.0 ms] // 选取 CSet
- [Ref Proc: 0.9 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] // 释放被回收区域的耗时 (包含他们的 RS)
- [Eden: 5120.0K(24.0M)->0.0B(12.0M) Survivors: 0.0B->2048.0K Heap: 16.0M(50.0M)->12.4M(50.0M)]
- [Times: user=0.01 sys=0.00, real=0.01 secs]
- // 根区域扫描
- 2018-05-03T10:21:43.213-0800: [GC concurrent-root-region-scan-start]
- 2018-05-03T10:21:43.214-0800: [GC concurrent-root-region-scan-end, 0.0012422 secs]
- // 并发标记
- 2018-05-03T10:21:43.214-0800: [GC concurrent-mark-start]
- 2018-05-03T10:21:43.214-0800: [GC concurrent-mark-end, 0.0004063 secs]
- // 重新标记又叫最终标记
- 2018-05-03T10:21:43.214-0800: [GC remark 2018-05-03T10:21:43.215-0800: [Finalize Marking, 0.0003736 secs] 2018-05-03T10:21:43.215-0800: [GC ref-proc, 0.0000533 secs] 2018-05-03T10:21:43.215-0800: [Unloading, 0.0007439 secs], 0.0013442 secs]
- [Times: user=0.00 sys=0.00, real=0.00 secs]
- // 独占清理
- 2018-05-03T10:21:43.216-0800: [GC cleanup 13M->13M(50M), 0.0004002 secs]
- [Times: user=0.01 sys=0.00, real=0.00 secs]
这是一段完整的 GC 日志. 从整体上看, 并发标记周期和混合回收的前后都有可能穿插着新生代 GC. 其中并发标记周期主要是回收老年代空间, 当然也包含了一次新生代 GC.
----------------------------------------------------------------
来源: https://www.cnblogs.com/yunxitalk/p/8987318.html