(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个对象,然会进入阻塞状态;
      1. public class Demo1 {
      2. public static void main(String[] args) throws InterruptedException {
      3. List<Data> datas = new ArrayList<Data>();
      4. for(int i=0; i<10000; i++){
      5. datas.add(new Data());
      6. }
      7. Thread.sleep(1*60*60*1000);
      8. }
      9. static class Data{
      10. }
      11. }
  • 获取 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[] 数组,这个数组占据了大量的内存;

image.png
image.png

  • Problem Suspect1 -> Details -> Accumulated Objects in Dominator Tree:
    • java.lang.Thread main 线程中引用了一个 java.util.ArrayList,这里面是一个 java.lang.Object[] 数组,数组里的每个元素都是 Demo1$Data 对象实例;
    • 这里就可以很清楚看到,到底是什么对象在内存里占用了过大的内存;

image.png

  • See stacktrace:追踪线程执行堆栈,找到问题代码;

    • 看到一个线程执行代码堆栈的调用链,追踪到到底哪个代码的执行才创建了这些对象;

      1. ![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)