文章目录

  • 1. 用Visual VM 加载堆转储文件
  • 2. 用Visual VM 分析堆转储文件
  • 3. 结合分析结果,定位并解决问题

1. 用Visual VM 加载堆转储文件

先将转储文件从服务器下载下来,打开Visual VM,点击右上角的Load Snapshot,将这个转储文件加载到Visual VM中。

java map内存溢出 java内存溢出问题排查_java

2. 用Visual VM 分析堆转储文件

1)首先看到是醒目的红色,这里标记了堆内存溢出的线程,我这里显示是Nacos的配置中心的一个更新本地配置的Updater线程导致堆内存溢出。
我本地debug发现,这个线程里边根本就不会创建太多对象。而且本地测试环境的配置文件数 、配置文件内容大小不会与生产环境相差太多,那么可以推测生产环境也不会创建太多的对象。因此可以进一步推测不是这个线程导致的内存溢出。实际情况可能是在此之前就已经内存不足了,只是在这个线程任务中GC已经不起作用了、实在是分配不了新对象,最终程序崩溃。

java map内存溢出 java内存溢出问题排查_java_02


2)不能直接从线程上看出问题所在,就只能去看java对象的分布情况了。

Class by Number of InstancesClasses by Size of Instances两个维度都可以看出byte[]char[]String 的对象个数及内存大小都是排在最前列的。
光看这几种对象其实没啥意义,因为它们是最常见的几种准标量,它们几乎在任何系统的任何时候都是数量最多、内存占用最大的,它们一般都是被其他复杂对象给引用着,所以我们主要还是得关注这些复杂对象
除了这个常见的准标量外,可以直观看到EnterpriseProjectDO 对象占用内存最大。看到这个Java类型的对象很多,我第一反应就是我负责的那个定时任务可能在运行,那个定时任务会创建大量的EnterpriseProjectDO对象。但后来又细想了下,白天那个定时任务不会运行,它的执行计划是每晚11点,后来在XxlJob的用户界面复查发现当时也没有其他人手动执行那个定时任务。

我只能扩大排查范围,后来在Dominators By Retain Size维度看到了关键信息

java map内存溢出 java内存溢出问题排查_java map内存溢出_03


点击Dominators By Retain Sizeview all按钮显示出一下结果

java map内存溢出 java内存溢出问题排查_内存溢出_04


从上图可以看出,用绿框标注出的对象是重点怀疑对象,这几类对象数量多、占用内存大,且有好几个还是GC Root。

  • (1) 先看排第一的ResultSetImpl,这说明此时数据库查询向java程序返回了很大的数据报文。
  • (2) 再看ArrayList#115426这个对象,其中的元素类型是EnterpriseMainAndCredential,因此需要关注某个代码逻辑会用到这个类。
  • (3) 然后看HashMap#26865这个对象,展开后发现,这个HashMap的key是Long类型、value是ArrayList(其元素类型是EnterpriseCredentialDO),这明显是一个Java 8 对集合进行stream分组的结果。因此得重点排查对项目中对EnterpriseCredentialDO的分组代码
  • (4) 再然后看ConcurrentHashMap#239这个对象,展开这个map发现,它的key是Bean名、value是Bean对象,可以推测出这个对象是DefaultListableBeanFactory中记录bean名到bean实例之间映射的字段singletonObjects ,因此这个对象对我们排查问题没啥参考意义。
  • (5) 最后看ArrayList$SubList#1这个对象,展开这个对象后发现它是元素类型EnterpriseMainDOArrayList 的子视图,这个java项目中有使用内存分页,所以需要注意项目中对EnterpriseMainDO 集合调用subList进行分页的代码逻辑

3. 结合分析结果,定位并解决问题

(1) 直接IDEA中搜索 EnterpriseMainAndCredential被引用的地方,很快就定位了一个代码位置,这个类只有那一个处被使用。这个接口没有传租户id,会导致查询所有的EnterpriseProjectDO,在这个接口中这个实体对应的数据库表的数据量是最大的,达到了30多万,反序列化为Java实体时会创建大量对象。

(2) 另外由于过滤条件复杂,主数据没法在数据库层做过滤,需要将所有的EnterpriseMainDO(数据量不大,总量才3000多条)查出,再做内存分页,所以在堆转储文件分析看到了元素类型是EnterpriseMainDOArrayList$SubList

(3) 代码中对EnterpriseCredentialDO EnterpriseProjectDO 都进行了stream 分组,所以在堆转储文件分析看到了上面提的的HashMap#26865对象。

(4) 内存溢出的原因也在于这里的stream 分组统计,代码中对这两对象集合分组后,只取了分组后的size,并未使用集合里的对象具体信息。EnterpriseProjectDO 的数据量特别大,且代码逻辑又只需要统计的count数,所以这个统计应该放在数据库上(利用group by统计)。

(5) 最后,我改写了代码,用sql在数据库上做数据统计,大大减少了数据库的返回数据报文大小,java程序客户端也就不需要创建那么多的java数据库实体类,不会出现GC失败、内存溢出的问题了。