一次内存溢出的排查心得

1,035 阅读2分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前言

今天突然收到了服务容器告警,内存达到了百分之九十的告警线了。正好前几天刚上了一个大需求,第一反应就是这个新模块的代码有内存泄漏点。本文将从如何发现内存泄漏点、如何定位内存泄漏点的代码到最后如何解决内存泄漏,说一说心得。

MAT

目前分析jvm内存问题最好的工具,我认为是Memory Analyzer。支持html页面查看内存情况。

image.png 通过这张预览图,如果有泄漏点的话,会有Problem Suspect提示出来,告诉你占了百分之多少。这里我们一眼就能看出是friend-thread-pool这个线程池里出了问题。这里推荐大家一个项目代码如果有多个业务模块的话,建议每个业务模块定义自己的线程池,这样不仅可以做到线程隔离,也方便了我们后期排查问题。这样我们就一下子把代码范围缩小到使用friend-thread-pool线程池的业务模块了。

点击“Details »”,查看详细信息

image.png 可以看出MemberSubDTO这个类有125,018个,ArrayList和CopyOnWriteArrayList占了大量的内存。不难分析出是List<MemberDTO>这块代码出了问题。这样就可以定位出准确的代码了

private void initAppAndHistorySub(Long accountId, List<MemberDTO> subList) {
    List<MemberDTO> historySubList = Lists.newCopyOnWriteArrayList();
    List<MemberDTO> onlineSubList  = Lists.newCopyOnWriteArrayList();
    subList.parallelStream().forEach(memberDTO -> {
        MemberDTO btUserDTO = UserFeignImpl.get().getBtEntryInfoByAccountId(memberDTO.getAccountId(), false);
        if (Objects.nonNull(btUserDTO)) {
            historySubList.add(memberDTO);
        } else {
            onlineSubList.add(memberDTO);
        }
    });
    List<MemberDTO> intersectionAppAndBtSubList = new ArrayList<>(onlineSubList);
    initOnlineSub(accountId, onlineSubList, intersectionAppAndBtSubList);
    initHistorySub(accountId, historySubList);
}

这里用了并发流处理list,考虑到线程安全问题,就用了CopyOnWriteArrayList。无视了CopyOnWriteArrayList的使用场景。正如CopyOnWriteArrayList的名字一样,是满足 CopyOnWrite 的 ArrayList,所谓 CopyOnWrite 的意思:就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。当写操作多时,就会一直内存拷贝,自然就OOM了。所以CopyOnWriteArrayList并不适合写多读少的场景,我们这里就是写多读少。
知道原因后就可以修改代码了,不用parallelStream就不存在线程安全,自然就不需要用CopyOnWriteArrayList了。

总结

在使用CopyOnWriteArrayList一定要考虑使用场景,不能一味了遇到线程安全问题就用CopyOnWriteArrayList。另外,每个业务模块定义自己专属的线程池,也是一个百利无一害的小技巧。