使用一段代码模拟出频繁 Full GC 的一个场景:
(1) 模拟代码的 JVM 参数
-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 分析程序运行的状态
- 程序运行起来后,jstat 每秒统计一次,YGC 每个一条增加一次 YoungGC 的次数,符合我们之前的 YoungGC 预期频率;
- YoungGC 后,S1U 中有 1M 左右的存活对象,应该是一些未知对象;
- YoungGC 后,OU 中多出来 30MB 左右的对象,因为 data1、data2、data3 在 Young GC 后存活,且Survivor 放不下,直接进入老年代;
- 老年代每秒新增 20MB~30MB,几乎每三秒触发一次 Full GC;
- 发现 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;
- 发现还是每秒触发一次 Young G,之后会有 20~30MB 的存活对象进入 Suvivor,但是每个 Survivor 都是 50MB,因此可以轻松容纳,而且一般不会过 50% 的动态年龄判定的阈值;
- 几乎没有对象进入老年代,最终只有1MB左右的未知对象进入老年代;