• 线上一个模块内存泄露了。通过一系列命令查看,有以下特征:
  • 并发时才会复现
  • 老年代居高不下
  • CPU占用一直往上飙升
  • 复现办法
@Test
public void testStream() {
    Long size = 1000000l;
    Map<String, String> map = new HashMap<>();
    List<String> list = new ArrayList<>();
    for (long i = 0; i < size; i++) {
        list.add(UUID.randomUUID().toString());
    }
    list.parallelStream().forEach(node -> map.put(node, node));
}
  • 使用工具
  • jmap -histo:live pid 可以看到不同的类的占用的空间大小
  • jstack 可以拿到当前栈的快照
  • 使用例子
  • 使用 jstack 命令, 将java进程所有的线程堆栈信息输出到文件:  jstack 14739 > stack.log
  • 使用命令统计各线程情况:
  • cat stack.log | grep "\.java" | grep explink | sort | uniq -c
  • 输出结果:
  • 12837         at cn.explink.b2c.weisuda.threadpool.SubExcuteWeisudaTask.run(SubExcuteWeisudaTask.java:76)
  • 表示有12837个线程是在执行 SubExcuteWeisudaTask.java 的 76 行代码。
  • jstat -gcutil pid interval(ms) gc查看
  • 使用命令,查看 JAVA 进程创建了多少线程:    ps -Te | grep java  | wc
  • jmap -dump:format=b,file=/tmp/dump 38648 dump内存信息然后分析:
  • https://gceasy.io/ 在线分析
  • 分析过程
  • 首先看有没有静态变量一直引用着并且不断膨胀:没有
  • 看看并发的情况下,有没有哪些地方会出问题:
  • jstack发现stream里用的parallelStream一直在运行:经过看一些文章,纯粹用parallelStream应该是没有问题的,问题可能出在parallelStream+hashmap.put
  • 再看看jstack,多个线程都是在运行hashmap put里。resize()的时候卡死了【是多个线程都这样吗?】
  • 如果真的是多个线程卡死在这,猜测可能的原因:
  • 多个线程对同一个hashmap进行插入。(具体为什么会发生问题其实不好确定。但是可以肯定的是:多线程,没有锁,都去写,肯定有问题,其实分析到这可以确定这肯定有问题,但是不一定是这个问题导致的内存泄露,可以先修这个问题了)
  • 下面猜想一下可能发生的情况
  • 多个线程都发现hashmap达到一定阈值的时候需要resize。java.util.HashMap#resize
  • A线程先申请新的区域,全局table设置成A申请的新区域,进行复制。
  • B线程也申请了新的区域,这时候拿到的全局table是A申请的新区域,还在复制中呢,根据这个内容复制出来的不是完整的。可是这也不会爆内存啊。【此路不通】
  • 还是想多线程同时put的问题,需要等到每一个都put完最终才能执行完这段代码
  • 有很多个put的时候发生了resize。
  • 复现:利用上面的代码,跑起来,发现停不下来。用idea debug,卡死的时候按暂停,发现一直在一个找根的代码里循环出不来。所以就是树成环了。java.util.HashMap.TreeNode#root
  • 问题解决
  • 最终就是通过将parallelStream换成stream解决了问题
  • 参考
  • 这里说ForkJoinPoll.CommonPoll是有内存泄漏问题的。
  • 这里有个例子可以证明hashmap不是线程安全的(一个线程写put的时候resize,东西还没复制完全呢,另一个线程读get不到)【但是这不会导致本文的问题】
  • 这里说,使用parallelStream一定要避免有些任务stuck了,任务都需要在一定的时间内完成:典型的不好的例子:任务里有网络请求