框架:
springboot2.1.3 + netty4.1.67 + activemq-client5.15.6
现象描述:
任务在运行时内存在不断升高,达到设置的-Xmx时,进程被kill。
排查流程:
一、netty内存泄漏:
首先第一个想到使用netty的ByteBuf未释放,导致内存泄漏,通过添加启动命令:-Dio.netty.leakDetection.level=PARANOID来定位未释放的ByteBuf。添加后发现没有任何异常输出,并不是netty导致。
二、替换垃圾收集器:
在任务被kill掉时,通过free -g 观察操作系统,发现内存并未被释放,依然被占用。所以怀疑是否因为垃圾收集器导致内存未归还给操作系统 ,通过命令:java -XX:+PrintCommandLineFlags -version查看默认垃圾收集器如下:
默认使用UseParallelGC 即 Parallel Scavenge + Parallel Old。添加启动命令:-XX:+UseG1GC将垃圾收集器改为G1收集器。 修改后,发现内存还是在持续增长,但是任务被kill之后,内存即时释放了,还未解决根本问题。
三、分析内存:
在服务器上安装arthas,启动任务通过dashboard命令观察实时面板,通过观察发现任务启动后,内存一直在实时增长,频繁触发youngGc,FullGc次数一直为0。老年代内存在小幅度增长,每次刷新增长几十M。当老年代内存到达上限时,触发了几次FullGc,且时间较长,但是内存并未释放,进程被kill。由此可判断是因为某个对象一直持续生成,且一直有引用无法被回收导致内存溢出。
检查代码后,未发现代码中有未释放的对象,开始分析dump日志。
四、分析dump.hprof
启动任务,待运行一段时间内存增长之后,在arthas中使用命令: heapdump /home/arthas/dump.hprof,使用mat分析dump日志。
1.选择leak Suspects,查看可能泄漏的对象。
2.发现在org.apache.activemq.ActiveMQMessageConsumer中存在一个LinkedList占用了90%的内存。
3.在下方Details信息里发现该对象名为deliveredMessages。
4.在详细信息中也可以观察到,在dump时,该LinkedList已经存储了53万多个对象,由此可见,该容器里的对象未被remove导致一直存在引用,无法被垃圾回收。
五、分析代码
由于org.apache.activemq.ActiveMQMessageConsumer是activeMq-client jar包中的类,所以我们先进行debug排查,首先搜索所有使用该变量的地方,尤其是添加和删除的地方,打上断点,开始调试。
1.第一次进入断点是在添加的地方,调用addFirst方法将MessageDispatch对象放入list中,根据堆栈信息发现是从receive方法中调用而来,即每次拉取数据都会调用该方法。
2.分析这段代码,当if中this.isAutoAcknowledgeBatch()成立后就会往list中存入对象,继续追踪isAutoAcknowledgeBatch()方法。此方法逻辑较为简单,判断消费到的消息是否需要批量ack并且不是Queue类型。如果返回false就会往deliveredMessages中存入MessageDispatch。由此可判断deliveredMessages中存入的是需要ack的信息。
3.查看session创建的地方,发现确实是设置批量自动ack。按道理来说,不应该再存入deliveredMessages中。
4.继续追踪 createSession方法,发现在transacted=true时,acknowledgeMode会设为0,即手动ack。至此真相大白,每次消费到数据都需要手动ack,deliveredMessages中记录了需要ack的消息,但是由于自认为设置的是自动提交,代码中并未手动提交,导致deliveredMessages一直持续增长至内存溢出。
5.因为任务中并未使用事务,所以将transacted改为false即可。上线后,内存正常了......
结尾:有时候内存溢出的不一定是自己写的代码,不要只盯着自己的代码看!