任何使用Java开发应用的开发者都会或多或少遇到OOM(OutOfMemoryError),这个问题的产生源自我们对程序设计的不合理以及对象无法及时释放所占空间导致的。往往此类问题较难排查,需要分析JVM堆内存中对象进行分析才能准确的定位根据原因。本文将介绍一个开发过程中遇到的一个问题,并讲解如何利用JVM工具以及MAT进行问题排查。
什么是OOM
在JVM没有足够的堆内存空间分配给新对象的时候,就会抛出java.lang.OutOfMemoryError错误。
为什么会发生OOM
通常,在没有足够的空间在Java堆中分配对象时,会抛出此错误。在这种情况下,对象申请的内存空间超过堆内存剩余空间,垃圾收集器无法回收足够的空间,并且无法进一步扩展堆内存空间。在极少数情况下,当垃圾收集器花费过多的时间,且释放的内存很少也可能导致OOM。
排查思路
假设在JVM内存空间足够的情况下发生了OOM,很可能就是代码中存在问题,导致对象所占空间无法被及时释放,从而引起应用发生OOM。所以需要分析JVM堆内存快照(heapdump),找出无法被释放的对象,确定问题发生的原因。
接下来用一个实际案例,讲述如何通过JVM工具以及MAT排查内存泄漏问题。
排查过程
程序执行一段时间后,抛出OOM错误,如下图。
开启VisualVM连接虚拟机,观察堆内存使用情况,可以发现内存使用持续增加。
通过
jstat -gc
观察虚拟机各分区垃圾回收情况。
由图中可以看出年轻代回收次数很多,但是内存能够正常回收,但是老年代回收内存基本没有释放,反而使用空间更大(最后两条GC日志)。然后通过
jmap
命令生成堆内存快照,通过MAT分析。
这里可以看出,有可疑对象占用了1.4GB内存,接下来查看可疑的泄漏对象
通过MAT的Leak Suspects(可疑的泄漏对象)发现,共有955个com.alibaba.fastjson.JSONObject
对象占用了91.85%的内存空间。从这里能够看出来应该是这个com.alibaba.fastjson.JSONObject
无法及时释放,导致的内存泄漏问题。那么为什么这些对象为什么无法被回收呢?
接下来通过MAT的Dominator Tree分析对象引用关系。
从图中可以看出,内存中有非常多的com.alibaba.fastjson.JSONObject
对象实例。随机选中一个实例,通过右键观察持有该对象引用的对象(list -> Incoming References)。
通过图中可以看出该对象被三个对象引用,而这三个对象又分别被别的对象引用,引用关系如下:
ArrayList @ 0x835c2110
HashMap @ 0x846e7608
Thread @ 0x8221e450 main
ArrayList @ 0x841a04b8
HashMap @ 0x83d64de0
Thread @ 0x83ecb138 data-sort-6
Thread @ 0x84056440 data-sort-4
Thread @ 0x840568a8 data-sort-1
ArrayList @ 0x846e75f0
Thread @ 0x8221e450 main
由此可以看出,该对象被三个ArrayList
对象持有引用,且这三个ArrayList
中的两个直接和间接的被Thread main
持有引用,另一个ArrayList
对象被三个子线程间接持有引用,由于子线程执行完毕之后会释放对象引用,故排除子线程。接下来主要观察主线程对该对象引用的持有情况,由于存在两出引用关系,且在主线程中分别被HashMap
和ArrayList
对象直接持有,故在代码中寻找两处引用的地方,最后发现,由于开发人员只释放了HashMap
对com.alibaba.fastjson.JSONObject
的引用,而ArrayList
并没有释放,最终导致程序在执行一段时间之后产生OOM错误。