(1) 案例问题
- 一次线上的大数据处理系统进行升级,半小时发现系统所在机器的 CPU 负载过高,直接导致机器宕机;
- 通过 jstat 发现,Full GC 非常频繁,两分钟一次 Full GC,每次 Full GC 耗时非常长,10s 左右;
- 通过 jstat 发现系统运行时内存模型:
- 堆内存 20G;
- 年轻代 10G,Eden 8G、每块 Survivor 1G;
- 老年代 10G;
- Eden 大概一分钟就会占满,触发一次 Young GC,会有几个GB的存活对象进入老年代;
- 老年代平均两分钟就会塞满,触发一次 Full GC,因为老年代内存很大,回收需要耗时10s;
- 推导原因:
- 系统运行时产生了大量的对象,而且处理的极其慢,经常在1分钟后触发 Young GC,之后有几个GB的存活对象进入老年代,平均两分钟塞满,触发 Full GC;
- 频繁且耗时很长的 Full GC,直接导致机器的 CPU 负载太高,系统直接卡死;
(2) 优化思路
- 之前的那套优化思路是否还能起作用?
- 根据系统运行时的内存模型,即使调大年轻代,给 Survivor 更多的内存空间,甚至达到2G~3G,但是一次 Young GC 后,还是会因为系统处理过慢,导致几个GB的存活对象,放不下 Survivor,进而直接进入老年代;
- 此时,就不是优化 JVM 参数就可以搞定了,需要进行代码层面的优化;
- 很可能是系统里代码有一定的改动,直接导致系统加载过多的数据到内存中,且对数据的处理还特别慢,在内存里几个GB的数据甚至要处理一分多钟,才能处理完毕;
- 代码层面的优化,需要定位系统里是什么对象占用太多内存,然后定位是执行了哪些程序才创建了这些对象;
- 经过 MAT 工具的分析, 发现是系统执行“String.split()”方法导致产生了大量的对象;
- JDK1.7 中,split 方法会将每隔切分出来的字符串创建一个新的数组;
- 这个大数据处理系统,在升级之后加了 String.split() 这个操作,可能一次性加载几十万条数据,数据主要是字符串,然后对这些字符串进行切割,每个字符串都会切割为 N 个小字符串,这导致字符串数量暴增几倍甚至几十倍,系统频繁产生大量对象;
- 优化方案:
- 避免代码层面加载过多的数据到内存里去处理;
- 优化掉 String.split();
- 开启多线程并发处理大量的数据,尽量提升数据处理的速度,避免 Young GC 后有过多的存活对象进入老年代;
- 避免代码层面加载过多的数据到内存里去处理;
(3) 基于 MAT 分析内存快照的示例
示例代码
- 代码里创建了 10000个对象,然会进入阻塞状态;
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
List<Data> datas = new ArrayList<Data>();
for(int i=0; i<10000; i++){
datas.add(new Data());
}
Thread.sleep(1*60*60*1000);
}
static class Data{
}
}
- 代码里创建了 10000个对象,然会进入阻塞状态;
获取 JVM 进程的 dump 快照文件 ```shell $ jps 18104 Demo1 1660 Jps
$ jmap -dump:live,format=b,file=dump.hprof 18104 Dumping heap to E:\dumps\dump.hprof … Heap dump file created ```
- 使用 MAT 分析内存快照
- LeaK Suspects Report 内存泄漏分析
- 提示可能存在的内存泄漏问题,例如 “Problem Suspect1,java.lang.Thread main”;
- 就是说 main 线程通过局部变量引用了占据 19.88% 内存的对象;
- 那是一个 java.lang.Object[] 数组,这个数组占据了大量的内存;
- 提示可能存在的内存泄漏问题,例如 “Problem Suspect1,java.lang.Thread main”;
- LeaK Suspects Report 内存泄漏分析
- Problem Suspect1 -> Details -> Accumulated Objects in Dominator Tree:
- java.lang.Thread main 线程中引用了一个 java.util.ArrayList,这里面是一个 java.lang.Object[] 数组,数组里的每个元素都是 Demo1$Data 对象实例;
- 这里就可以很清楚看到,到底是什么对象在内存里占用了过大的内存;
See stacktrace:追踪线程执行堆栈,找到问题代码;
看到一个线程执行代码堆栈的调用链,追踪到到底哪个代码的执行才创建了这些对象;
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1471554/1628758058872-9e504337-6e0f-4062-98da-c83da8d17059.png#align=left&display=inline&height=110&margin=%5Bobject%20Object%5D&name=image.png&originHeight=147&originWidth=766&size=14780&status=done&style=shadow&width=575)