程序报错
10:06:01 [WARN ] i.n.util.concurrent.DefaultPromise java.lang.OutOfMemoryError: Java heap space
出现场景
程序导出数据库中数据,导出5W数据,每次查询1000.
for循环中创建大量对象,导致内存溢出,怀疑短时间内创建大量对象,内存回收不及时导致堆溢出。
预测需要内存大小
System.setProperty("java.vm.name","Java HotSpot(TM) ");
System.out.println(ObjectSizeCalculator.getObjectSize(dataList));
可以得出对象占用大小,
单次循环内 1000条数据占用内存 2,345,568 。 约 2.3M。
需要导出5w条数据,循环50次。 每次循环结束,创建的对象都会失效回收,最差情况需要最大内存 115M
查看机器中Java程序堆内存的默认初始大小和最大大小
-Xmx 最大堆内存的默认值为当前机器最大内存的 1/4
-Xms 初始堆内存的默认值为当前机器最大内存的 1/64
在Windows里:
java -XX:+PrintFlagsFinal -version | findstr /i "HeapSize PermSize ThreadStackSize"
在Linux里:
java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize'
可以看出最大堆内存为503M,初始堆内存为33M,我们的机器内存为2G,符合上述比例。
-Xms128 -Xmx512m
报错:Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
原因:
执行垃圾收集的时间比例太大,有效的运算量太小。默认情况下,如果GC花费的时间超过 98%,并且GC 回收的内存少于 2%,JVM 就会抛出这个错误。
假如不抛出 GC overhead limit 错误会发生什么情况呢? 那就是 GC 清理的这么点内存很快会再次填满,迫使 GC 再次执行。这样就形成恶性循环,CPU 使用率一直是 100%,而 GC 却没有任何成果。系统用户就会看到系统卡死。以前只需要几毫秒的操作,现在需要好几分钟才能完成。
分析:每次for循环结束,都会GC,但是每次回收的内存很少,频繁GC导致CPU利用率过高报错。
解决办法:
-XX:-UseGCOverheadLimit 禁用这个检查,只是错误信息延后,替换成 java.lang.OutOfMemoryError: Java heap space-Xms256m -Xmx2560m
测试正常,但服务发生多次 Full GC,严重影响性能。
初步判断为老年代空间不足引发Full GC。
由于服务主要是循环创建大量对象导致内存问题,每次循环后创建的对象都将失效,部分对象会进入老年代导致老年代空间不足,触发Full GC
解决方法:
减少Full GC,增加Minor GC,调整新生代与老年代比例,增大新生代内存。
设置 -XX:NewRatio, 默认为2,代表新生代与老年代内存比为1:2,新生代占1/3
-XX:NewRatio=1
再次测试,发现Full GC次数大幅减少
- Minor GC :清理新生代,Minor GC是最频繁触发的GC,速度也最快的;
- Major GC :清理老年代,Major GC比Minor GC慢十倍以上,单独触发Major GC只有CMS和G1有这个能力,因为cms和G1处理老年代不需要触发full gc,其他的老年代回收器回收老年代需要触发full gc。
- Full GC: 清理整个堆空间,包括新生代、永久代和老年代,它会启动老年代收集器和新生代收集器一起工作,全程Stop The World,尽量避免Full GC
Full GC会回收整个堆的内存,包含老年代,新生代,metaspace等,GC的全过程中所有用户线程都是处于暂停的状态。
触发条件:
- 老年代空间不足
- 永久代/metaspace内存空间不足
- 调用System.gc()会建议系统调用full gc
-XX:MaxTenuringThreshold设置阈值-XX:PretenureSizeThreshold设置