04 G1垃圾回收器的介绍以及垃圾回收调优的基础知识和简单案例

1 G1(garbage one/first)垃圾回收器的基础知识

1-1 概述

JDK9废弃了之前的CMS(concurrent mark sweep)垃圾回收器,将G1回收器作为默认垃圾回收器。

应用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,基本思想就是将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个Region之间是 复制 算法

相关JVM参数

-XX:+UseG1GC                           // JKD7,8需要使用这个参数开启G1垃圾回收器
-XX:G1HeapRegionSize=size              // 设置Region的大小
-XX:MaxGCPauseMillis=time              // 每次垃圾回收的最大暂停时间

1-2 G1垃圾回收器的阶段

E:eden S:swap O:old

总体上分为三个阶段(这三个阶段是循环执行的):

1)Yong Collection: 新生代的垃圾收集.

2)Young Collection + Concurrent Mark: 新生代的垃圾收集+并发标记

3)Mixed Collection:混合收集

总结:刚开始阶段,垃圾比较少,仅仅1)只有新生代的垃圾收集。程序运行一段时间后,老年代的垃圾超过了一定的阈值,这个时候在2)对新生代进行垃圾回收的同时还会对老年代的垃圾进行并发的标记。之后进行一个3)混合的垃圾回收,在混合的垃圾回收后,Eden区的垃圾会被回收完毕再次进入阶段1)

1-2-1 Young Collection阶段

特点新生代的垃圾回收会触发STW阻塞用户线程)!!!!!!!!!

stage1:左图:刚开始对象都被分配在Eden区。

stage2:右图:经过一段时间后,Eden区内存紧张进行Young Collection,幸存的对象会被被拷贝到幸存区(s)

stage3:当Eden区存活的对象越来越多并且幸存区中对象存活的时间达到一定阈值

  • 这个时候部分对象会被放到老年代(o)(红色箭头)
  • 不够年龄的依旧从from幸存区拷贝到to幸存区,然后交换from与to指向。

1-2-2 Young Collection + CM阶段

特点

  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

注意点:区分初始标记以及并发标记的时机,初始标记不会占用并发标记时间

-XX:InitiatingHeapOccupancyPercent=percent  // 默认45%,老年代占用堆空间达到45%时进行并发标记

1-2-3 Mixed Collection阶段

会对 E、S、O 进行全面垃圾回收

1)对于老年代的垃圾回收也会采用复制垃圾回收算法,值得注意点是G1为了满足吞吐量的指标,有时会有选择的对老年区的对象进行垃圾回收(挑选回收价值高的区域即尽可能回收多的垃圾),并不会对所有对象进行清理。

混合标记包含以下步骤:

  • 最终标记(Remark)会 STW:标记一些遗留的垃圾对象,并发标记的过程中用户线程还会有新的垃圾产生。
  • 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms        // 单次垃圾回收的最长时间

G1名称的由来?

主要目的:G1垃圾回收器的核心思想就是优先回收垃圾最多的区域。目的是为了让垃圾回收暂停时间最短,从而达到高的吞吐量。

1-3 JVM的 Full GC 与 Minor GC 在各种垃圾回收器的体现

  • SerialGC (串行垃圾回收器)
1)新生代内存不足发生的垃圾收集 - minor gc
2)老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC(并行垃圾回收器)
1)新生代内存不足发生的垃圾收集 - minor gc
2)老年代内存不足发生的垃圾收集 - full gc
  • CMS(Concurrent Mark Sweep回收器)
1)新生代内存不足发生的垃圾收集 - minor gc
2)老年代内存不足
  • G1
1) 新生代内存不足发生的垃圾收集 - minor gc
2) 老年代内存不足

总结:

  • 对于上述四种垃圾回收器,新生代的内存不足发生的垃圾回收都是Minor GC。

  • 对于串行与并行的垃圾回收器,老年代内存不足就是full GC。

  • 对于CMS以及G1垃圾回收器,老年代内存不足的垃圾回收需要分情况讨论。

G1为例,老年代垃圾回收会出现两种情况:
1)回收速度 > 垃圾产生速度, 不是full GC,此时是concurrent GC。
2)垃圾产生速度 > 回收速度,这时候并发GC会失败,会退化成为full GC(垃圾回收日志中会出现full GC字样),并且这种情况下
STW时间会较长。

1-4 新生代垃圾回收的跨代引用(目的:提高新生代垃圾回收的速率)

新生代回收的跨代引用概念: 老年代区域的对象引用新生代 区域的对象

基本思想:新生代垃圾回收的过程需要寻找root对象进行可达性分析之后找到存活对象,然后将存活对象使用复制算法进行垃圾回收。

  • 问题:root对象必定有一部分在老年代的区域老年代存活对象较多,如果遍历老年代去确定根对象,效率上比较低,如何加快新生代垃圾收集时,根对象的确定?

  • 策略:采用card table技术,将老年代的内存空间进行进一步细分(512KB左右),如果老年代中对象引用了新生代中对象,对应的card存储区域则标记为“脏卡”。在新生代垃圾回收时,只需要遍历老年代的脏卡区域就可以了,减少搜索范围,提升了效率

  • 细节

    • 1)Eden区的对象会通过一个叫 rember set的结构记录老年区的脏卡位置
    • 2)脏卡的标记通过写屏障实现,将更新脏卡的指令放入队列(在引用变更时通过 post-write barrier + dirty card queue ),有专门的线程进行更新(concurrent refinement threads 更新 Remembered Set )。

1-5 G1垃圾回收器Remark阶段的详情

为什么需要remark?

主要原因在于并发标记的时候,垃圾标记线程与用户线程是并发执行的,在最后清理前,可能会产生误判,比如对象C刚开始被标记为垃圾,但是在并发执行过程中,对象C又再次被根对象A引用了,因此对象C就不是垃圾了,对于这种类似于对象C的情况,我们需要通过remark机制确保最终垃圾回收的对象确实是垃圾

细节:当C对象引用放生改变时,JVM会给A的属性加入写屏障,这个写屏障会将C加入到队列当中并将这个对象标记为未处理的状态(图中灰色)。

在remark阶段,remark线程会将队列中对象拿出来重新处理,判断这些对象到底是不是垃圾对象。

核心技术:pre-write barrier(写屏障) + satb_mark_queue(队列)

2 G1垃圾回收器的优化知识

2-1 字符串去重

具体做法:将所有新分配的字符串放入一个队列 ,当新生代回收时,G1并发检查是否有字符串重复,如果它们值一样,让它们引用同一个 char[] (注意这里时char数组对象不是String对象)

与串池StringTable的区别

  • String.intern() 关注的是String对象,字符串去重关注的是 char[],让相同的char [] 数组共享引用
JDK8中String的底层是char []。

优缺点

  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

这个优化默认是开启的,JVM参数是

-XX:+UseStringDeduplication

2-2 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸
载它所加载的所有类 -XX:+ClassUnloadingWithConcurrentMark 默认启用

2-3 回收巨型对象

  • 一个对象大于 region 的一半时,称之为巨型对象,G1 不会对巨型对象进行拷贝,回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生
    代垃圾回收时处理掉。incoming引用是指新生代中巨型对象对应的老年代的卡表引用,卡表引用为0,则回收新生代巨型对象

2-4 并发标记起始时间的调整

并发标记必须在堆空间占满前完成,否则退化为FullGC,必须要保留一些空白空间用于存储浮动垃圾。
JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 
JDK 9 可以动态调整
    -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    进行数据采样并动态调整
    总会添加一个安全的空档空间

2-5 垃圾回收更多的参考资料

Oracle官方文档

3 GC调优的基础知识

3-1 预备知识

掌握 GC 相关的 VM 参数,会基本的空间调整
掌握相关工具包括jconsole工具,jmap等工具
原则:调优跟应用、环境有关,没有放之四海而皆准的法则

3-2 GC调优目标确定

根据目标选择合适的垃圾回收器?
  • 对于科学运算项目可能需要的是高吞吐量,对于互联网项目可能需要的是低延迟。
  • 响应时间优先的垃圾回收器:CMS,G1,ZGC(其中CMS是业界广泛使用的垃圾回收器,JDK9中将G1垃圾回收器作为默认垃圾回收器,G1垃圾回收器在大内存场景要比CMS强)
  • 吞吐量优先的垃圾回收器:ParallelGC

3-3 进行GC调优前需要排除的问题

1) 数据是不是太多?
比如代码中数据库查询对整张表进行查询。
2)数据表示是否太臃肿?
----考虑对象设计,查询对象的所有属性信息是否都必须用到
----考虑对象的大小,最小的对象仅只包含对象头则只包含16个字节,int类型是4个字节,能用基本类型不用包装类型
3)代码中是否存在内存泄漏?
定义Map,只存放,不释放。
解决策略
1)软弱引用
2)使用第三方缓存实现比如redis,使用堆外内存。

3-4 新生代调优策略

新生代分配对象的特点:

  • 所有的 new 操作的内存分配非常廉价

    • TLAB thread-local allocation buffer:优先使用TLAB分配对象
  • 死亡对象的回收代价是零

  • 大部分对象用过即死,Minor GC 的时间远远低于 Full GC

新生代分配的内存是否越大越好?
Xmn Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
  • 新生代内存空间过小会造成大量的minor GC。
  • 新生代的内存空间过大,会造成老年代可用空间变少,这样会造成只会发生 full GC
  • 官方推荐新生代内存空间占比在25%~50%之间。

总的原则

  • 尽可能将新生代空间分配大一点
  • 新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区的大小设置原则
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置得当,让长时间存活对象尽快晋升
    • 存活时间短的对象如果由于幸存区比较小,较少的进入老年代,会造成这些对象只有full GC的时候才会被回收,对象的清理代价变高,本来在minor GC就该处理完的。

晋升阈值相关的JVM配置参数:(Tenurin)

-XX:MaxTenuringThreshold=threshold         // 最大的  晋升阈值(TenuringThreshold)
-XX:+PrintTenuringDistribution             // 打印晋升信息

实际JVM垃圾回收信息

Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total           //age代表该对象存活的年龄
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
...

3-5 老年代调优

经验规则

1)CMS 的老年代内存越大越好

  • 目的是预留足够的空间给并发产生的浮动垃圾,避免并发垃圾清理失败从而退化成串行的垃圾清理。

2)老年代调优前先分析垃圾回收的日志信息,如果没有full GC出现,说明老年代不是瓶颈,应优先开展新生代调优。

3)观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3


相关JVM参数

-XX:CMSInitiatingOccupancyFraction=percent    
// 老年代的空间占用达到总内存的多少时,进行CMS垃圾回收,值越低,垃圾回收的越早,一般设为75%,即预留25%空间给
// 浮动垃圾

4 GC调优的案例

案例1 Full GC 和 Minor GC频繁

分析

GC频繁说明堆空间内存不足。首先想到是新生代的堆空间内存不足,当业务量变大的时候,大量生命周期的短的的对象被创建,但由于新生代空间不足,部分对象的生命周期还没达到晋升阈值就进入了老年代。从而造成老年区的堆空间也短缺。造成频繁的 Full GC 和 Minor GC。

解决策略

  • 增大新生代内存空间
  • 增大晋升阈值

案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

分析

这个业务的需求要求高响应时间,因此采用了CMS作为垃圾回收器,首先分析是CMS哪个阶段导致该问题的发生。
CMS的标记可以分为三个阶段,分别是初始标记,并发标记,最终标记三个阶段。因此需要查看垃圾回收日志,从而
进一步确定原因。

解决策略

CMS垃圾回收器的常识

通过分析日志,发现重新标记阶段时间比较长。重新标记的工作量太大了。重新标记需要扫描新生代和老年代。
可以调整下面的JVM参数减少重新标记时间,即重新标记前对新生代垃圾对象进行清理。
-XX:CMSInitiatingOccupancyFraction=percent // 执行垃圾回收的内存占比,预留空间给浮动垃圾
-XX:+CMSScavengeBeforeRemark
// 在重新标记前,对新生代进行垃圾回收,减少并发清理的垃圾对象,+开启,-关闭

案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

原因分析

1.7版本方法区是采用永久带实现,并且当永久带空间不足的时候,会触发整个堆内存的垃圾回收。

补充知识点:JDK1.8方法区采用元空间的方式,不属于堆内存,因此不受垃圾回收器影响。

解决策略

增加永久代的初始值与最大值

参考资料

01 JVM基础课程

02 Oracle的JVM官方文档(重要)

原文地址:https://www.cnblogs.com/kfcuj/p/14649881.html