JVM性能优化(三)G1垃圾收集器

1,656 阅读9分钟

文章预习:JVM性能优化(二)垃圾回收算法详解

一、简介

G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS

G1的设计 原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  1. 第一步:开启G1垃圾收集器
  2. 第二步:设置堆的最大内存
  3. 第三步:设置最大的停顿时间

G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC ,在不同的条件下被触发。

二、原理

G1垃圾收集器相比于其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域
这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
在这里插入图片描述
在这里插入图片描述
在G1划分区域中,年轻代垃圾收集器依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间,G1收集器通过将对象从一个区域复制到另一个区域,完成了清理工作。

这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了,

在G1中,有一种特殊的区域,叫Humongous区域。

  • 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象
  • 这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。
  • 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象,如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区存储,为了能找到连续的H区,有时候不得不启动 Full GC

三、Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。

  • Eden 空间的数据移动到Survivor 空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间
  • Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中
  • 最终Eden空间的数据为空,GC停止工作,应用线程继续执行

在这里插入图片描述

3.1、Remembered Set(已记忆集合)

在GC年轻代的对象时,我们如何找到年轻代中对象的根对象呢?
根对象可能是在年轻代中,也可以在老年代中,那么老年代的所有对象都是根吗?
如果全量扫描老年代,那么这样扫描下来会耗费大量的时间
于是,G1引进了Rset的概念,它的全称是 Remembered Set,其作用是跟踪执行某个堆内的对象引用
在这里插入图片描述
每个Region初始化时,会初始化一个remembered set(已记忆集合),这个翻译有点拗口,以下简称RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。

3.2、Mixed GC

当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,既Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择那些Old region 进行收集,从而可以对垃圾回收的耗时时间进行控制,也要注意的是Mixed GC并不是Full GC
Mixed GC什么时候出发?由参数 -XX:InitiatingHeapOccupancyPercent=n决定。默认:45%,该参数的意思是:当老年代大小占用整个堆大小百分比达到该阈值时触发。

它的GC步骤分两步:
1. 全局并发标记(global concurrent marking)
2. 拷贝存活对象(evacuation)

3.2.1 全局并发标记

全局并发标记,执行过程分为五个步骤:

  • **初始标记(initial mark ,STW):**标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿。

  • 根区域扫描(root region scan):

    1. G1 GC在初始标记的存活区扫描对老年代的引用,并标记被引用的对象
    2. 该阶段与应用程序(非STW)同时运行,并且只有完成该阶段后,才能开始下一次STW年轻代垃圾回收。
  • 并发标记(Concurrent Marking): G1 GC在整个堆中查找可访问的(存活的)对象,该阶段与应用程序同时运行,可以被STW年轻代垃圾回收中断

  • 重新标记(Remark,STW): 该阶段是STW回收,因为程序在运行,针对上一次的标记进行修改。

  • 清楚垃圾(Cleanup,STW): 轻点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集,等待evacuation阶段来回收

3.2.2 拷贝存活对象

Evacuation 阶段是全暂停的,该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。

四、G1 收集器相关参数

  • -XX:+UseG1GC: 使用G1垃圾收集器

  • -XX:MaxGCPauseMillis: 设置期望达到最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒

  • -XX:G1HeapRegionSize=n:

    1. 设置的G1区域的大小,值是2的幂,范围是 1MB 到 32MB之间,目标是根据最小的Java堆大小划分出约2048个区域
    2. 默认 是堆内存的1/2000
  • -XX:ParallelGCThreads=n: 设置STW 工作线程数的值,将 n的值设置为逻辑处理器的数量,n的值与逻辑处理器的数量相同,最多为8

  • -XX:ConcGCThreads=n: 设置并行标记的线程数,将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右

  • **-XX:InitiatingHeapOccupancyPercent=n: **设置出发标记周期的java堆占用率阈值,默认占用率是这个Java堆的45%

五、测试

5.1 测试代码:

public class TestGC {

    // 实现:不断的产生新的数据(对象),随机的去废弃对象(垃圾)
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        while (true){
            int sleep = new Random().nextInt(100);
            if(System.currentTimeMillis() % 2 ==0){
                //当前的时间戳,是偶数
                list.clear();
            }else{
                //向List中添加1000个对象
                for (int i = 0; i < 10000; i++) {
                    Properties properties = new Properties();
                    properties.put("key_"+i,"value_"+System.currentTimeMillis()+i);
                    list.add(properties);
                }
            }
            Thread.sleep(sleep);
        }
    }
}

5.2 测试参数:

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xmx256m

在这里插入图片描述

5.3 日志输出:

[GC pause (G1 Evacuation Pause) (young), 0.0027884 secs]
   [Parallel Time: 2.2 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 542.1, Avg: 542.1, Max: 542.2, Diff: 0.1]
      # 扫描根节点
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 2.1]
      # 更新RS区域所消耗的时间
      [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.0, Diff: 0.0, Sum: 0.1]
      # 对象拷贝
      [Object Copy (ms): Min: 1.6, Avg: 1.8, Max: 1.9, Diff: 0.4, Sum: 14.6]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]
         [Termination Attempts: Min: 1, Avg: 5.5, Max: 7, Diff: 6, Sum: 44]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
      [GC Worker Total (ms): Min: 2.1, Avg: 2.1, Max: 2.2, Diff: 0.1, Sum: 17.2]
      [GC Worker End (ms): Min: 544.2, Avg: 544.2, Max: 544.2, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms] # 清空CardTable
   [Other: 0.5 ms]
      [Choose CSet: 0.0 ms] # 选取 CSet
      [Ref Proc: 0.4 ms] # 弱引用、软引用的处理耗时
      [Ref Enq: 0.0 ms]	# 弱引用、软引用的入队耗时
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms] # 大对象区域注册耗时
      [Humongous Reclaim: 0.0 ms]  # 大对象区域回收耗时
      [Free CSet: 0.0 ms]
   [Eden: 12.0M(12.0M)->0.0B(13.0M) Survivors: 0.0B->2048.0K Heap: 12.0M(252.0M)->3282.5K(252.0M)] # 年轻代的大小统计
 [Times: user=0.00 sys=0.00, real=0.00 secs] 

五、对于G1垃圾回收器优化建议

  • 年轻代大小:

    1. 避免使用 -Xmn选项或 -XX:NewRatio 等其他相关选项显示设置年轻代大小
    2. 固定年轻代的大小会覆盖暂停时间目标
  • 暂停时间目标不要太过严苛

    1. G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间
    2. 评估G1 GC的吞吐量时,暂停时间目标不要太严苛,目标太过严苛表示你愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量

六、可视化GC日志分析工具

6.1 GC日志输出参数

前面通过 -XX:PrintGCDetails可以对GC日志进行打印,我们就可以在控制台查看,这样虽然可以查看GC的信息,但是并不直观,可以借助于第三方的GC日志分析工具进行查看

在日志打印输出设计到的参数如下:

  • -XX:+PrintGC:输出GC日志
  • -XX:+PrintGCDetails:输出GC的详细日志
  • -XX:+PrintGCTimeStamps:输出GC的时间戳(以基准的时间的形式)
  • -XX:+PrintGCDateStamps:输出GC的时间戳(以日期的形式,如: 2020-05-04T21:25.234+0800)
  • -XX:+PrintHeapAtGC:在进行GC的前后打印出堆的信息
  • -Xloggc:…/logs/gc.log 日志文件的输出路径

测试参数:

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:E://test//gc.log

运行main 方法后,我们就可以在 E://test//gc.log下发现一个 gc.log 的日志文件:
在这里插入图片描述

6.2 GC Easy 可视化工具

GC Easy是一款在线可视化工具,易用、功能强大,可以通过GC日志分析进行内存泄露检测、GC暂停原因分析、JVM配置建议优化等功能,而且是可以免费使用的
网站地址:gceasy.io
在这里插入图片描述
上传gc.log后,点击Analyze,就可以查看日志报告了

JVM的各个分代区域分配的内存及使用峰值的内存
在这里插入图片描述

关键性能指标: 吞吐量及GC暂停平均时间、最大时间、各个时间段的比例

  • Throughput表示的是吞吐量

  • Latency表示响应时间

    1. Avg Pause GC Time 平均GC时间
    2. Max Pause GC TIme 最大GC时间
      在这里插入图片描述

第一部分是Heap after GC,GC后堆的内存图,堆是用来存储对象的
在这里插入图片描述

第二部分是Heap before GC,这是GC前堆的使用率,可以看出随着程序的运行,堆使用率越来越高,堆被对象占用的内存越来越大。
在这里插入图片描述
第三部分是GC Duration Time,就是GC持续时间。从图中可以看到,发生Full GC的时间持续的比较短,而Young GC持续的时间比较长。图中横坐标表示GC发生的时间段,纵坐标表示的是GC持续时间。

在这里插入图片描述
表示的是GC回收掉的垃圾对象的内存大小。在这里插入图片描述
GC Statistics
在这里插入图片描述
**Reclaimed Bytes(gb):**表示的是堆内存中Minor GC和Full GC回收垃圾对象的内存。
GC cumulative Time(ms) :总计GC时间,包括Minor GC和Full GC,时间单位为ms。
GC Average Time(ms) :GC平均时间,包括了Minor GC和Full GC。

在这里插入图片描述
总GC统计
MinorGC的统计
FullGC的统计
GC暂停程序的统计

七、结束语

到这里G1垃圾回收器就讲完了,感兴趣的小伙伴记得点赞关注,大家加油!