使用一段代码模拟出频繁 Full GC 的一个场景:

(1) 模拟代码的 JVM 参数

  1. -XX:NewSize=104857600 -XX:MaxNewSize=104857600 -XX:InitialHeapSize=209715200 -XX:MaxHeapSize=209715200 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=20971520 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
  • 堆内存 200MB;
  • 年轻代 100MB,Eden 80MB,每块 Survivor 10MB;
  • 老年代 100MB;
  • 大对象阈值 20MB;

(2) 示例程序

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(30000);
        while (true){
            loadData();
        }
    }
    private static void loadData() throws InterruptedException {
        byte[] data = null;
        for(int i=0; i<4; i++){
            data = new byte[10*1024*1024];
        }
        data = null;

        byte[] data1 = new byte[10*1024*1024];
        byte[] data2 = new byte[10*1024*1024];

        byte[] data3 = new byte[10*1024*1024];
        data3 = new byte[10*1024*1024];

        Thread.sleep(1000);
    }
}
  • 每秒执行一次 loadData();
    • 创建4个10M的数组,立刻变为垃圾对象;
    • 创建2个10MB的数组 data1、data2,接着 data3 要创建1个10MB数组,再加上一些未知对象,Eden 占用超过了70MB左右,接着 data3 再次创建1个10MB的数组,Eden的可用空间不足10MB,触发一次 Young GC;
  • 在一秒内触发 Young GC;

(3) 基于 jstat 分析程序运行的状态

image.png

  • 程序运行起来后,jstat 每秒统计一次,YGC 每个一条增加一次 YoungGC 的次数,符合我们之前的 YoungGC 预期频率;
  • YoungGC 后,S1U 中有 1M 左右的存活对象,应该是一些未知对象;
  • YoungGC 后,OU 中多出来 30MB 左右的对象,因为 data1、data2、data3 在 Young GC 后存活,且Survivor 放不下,直接进入老年代;
  • 老年代每秒新增 20MB~30MB,几乎每三秒触发一次 Full GC;

image.png

  • 发现 Young GC 的平均耗时比 Full GC 的还要多,因为 每次 Full GC 都是由 Young GC 触发的,YoungGC 后很多存活对象要放入老年代,老年代内存不够了才触发 Full GC,必须等 Full GC 执行完毕了,Young GC 才能把存活对象放入老年代,才算 Young GC 执行完毕,这也导致 Young GC 速度非常慢;

(4) 对 JVM 性能进行优化

  • 最大的问题:每次 Young GC 过后存活对象太多了,导致频繁进入老年代,频繁触发 Full GC;
  • 需要调大年轻代的内存空间,增加 Survivor 的内存:

    -XX:NewSize=209715200 -XX:MaxNewSize=209715200 -XX:InitialHeapSize=314572800 -XX:MaxHeapSize=314572800 -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=20971520 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
    
  • 堆内存:300MB;

  • 年轻代 200MB,同时 2:1:1,Eden 100MB,每个 Survivor 50MB;
  • 老年代 100MB;

image.png

  • 发现还是每秒触发一次 Young G,之后会有 20~30MB 的存活对象进入 Suvivor,但是每个 Survivor 都是 50MB,因此可以轻松容纳,而且一般不会过 50% 的动态年龄判定的阈值;
  • 几乎没有对象进入老年代,最终只有1MB左右的未知对象进入老年代;