引言
Java 开发者一般不需要考虑内存释放问题,全交由 GC 去处理。但是在一些生产环境中,JVM 经过长时间运行后,即使是一些很小的未释放的 Java 对象,日积月累也会导致内存资源枯竭,最终使 Java 应用崩溃的问题。本文将就一个 AIX 平台上基于 IBM JDK 开发的 Java 应用内存枯竭的实际案例分析过程,来引领读者理解基于 IBM JDK 的 Java 应用内存泄漏调查方法,以及分析思路。
第一步,判断是否是内存泄漏问题
根据生产环境出现的错误日志以及 GC 日志文件,进行初步判断是否是内存泄露问题。
Java 应用的错误日志:
“***WARNING*** Java heap is almost exhausted: 4% free Java heap
应用程序中对可用内存做了判断,当可用内存比较低的时候输出了 WARNING 的日志。
使用 IBM pattern modeling and Analysis Tools for Java Garbage Collector 来分析 GC 日志。
图 1. 选择打开 IBM JDK 的 GC 日志文件
图 2. 点击 Graph View Part 显示
图 3. 显示 GC 分析图
从图中可以看出 Java 内存的堆 (Heap) 的使用情况是持续的上升趋势。
由此我们可以得出结论,Java 应用程序存在内存泄漏问题,导致内存堆得不到释放。
第二步,截取 Java 内存堆的转存储文件
在得出是内存堆泄漏的问题结论后,接下来就需要取得内存堆的转存储文件来做进一步分析。
在 AIX 平台上截取 IBM JDK 的内存堆的转存储文件前,需要先对 IBM JDK 的 JVM 参数进行设置。有 2 种设置方式:
设置 IBM JDK 的全局变量:
export IBM_HEAPDUMP=true
添加 JVM 启动参数:
-Xdump:system+heap+java:events=user,request=exclusive+prepwalk+compact
设定完后需要重启 JVM, 使设定生效。然后可以在 kill -QUIT pid 命令来生成转存储文件 (Dump),pid 为实际启动的 JVM 进程 ID。
当内存泄漏情况非常小且缓慢的时候,无法从 1 个或 2 个转存储文件中分析出导致泄漏的 Java 对象。根据上面 GC 的日志趋势,制定如下的转存储文件的截取的方案。
截取周期为 1 星期以上,每天一次。
每天固定时间截取,且避开发生大的 GC 的时间段。
这样可以得到几个可以用来比对分析的转存储文件,以及避免正在运行中得一些 Java 对象对于分析的干扰。
第三步,分析转存储文件
使用 MAT (Memory Analyzer Tool) 工具来分析转存储文件。由于实际转存储文件非常大,需要调整 MAT 工具的启动参数文件(MemoryAnalyzer.ini),32 位的 window 平台的话,最大也只能设定到 1.5G。因此当分析超大的转存储文件时,建议在 64 位 window 平台上做,这样可以分配更多的内存给 MAT 工具使用。
1)查找可疑泄漏点
在 MAT 的 Overview 中,可以点击”Leak Suspect”来生成 Leak Suspect Reports, 做最直观的分析。
图 4. 点击 Leak Suspect
图 5. 显示某 1 天的转存储文件分析结果。
如果连续几天的转存储文件中,都是这个 Suspect 实例 (Instance) 的所占比例最大,且所占内存空间也在不断上升,没有下降的趋势的话,那基本上可以断定该实例是发生泄漏的对象了。
点击打开该 Suspect 的 Detail 信息。
图 6. 点击 Details 链接
通过比对连续几天的转存储文件,可以发现是 Hashtable 中得 Entry 对象的占用空间不断变大。
图 7. 显示 Detail 信息
那接下来进一步深入分析,到底在 Hashtable 中占用空间增大到底是什么实例。
2)深入分析
点击 Suspect 实例,打开该实例的 Dominator Tree。
图 8. 选择 Dominator Tree 选项
可以在 Dominator Tree 中看到 Hashtable 中放的 Java Instance,依次为
Company[] -> Event[] -> Task (Manager, Handler, xxxxx)
图 9. 显示 Dominator Tree 信息
分析其中 1 个复杂的 Task,点击 Path to GC Roots 继续深入分析 Task 的引用关系。Weak 和 Soft 引用会在 Major GC 是被释放,所以查看下不包含他们的引用关系。
图 10. 显示可疑点的引用关系图
根据 Java 应用的代码调查,Company 和 Event 是常驻于 Service 静态实例中。
引用 A 代码分析
引用 A 的顺序 Task <- Thread <- Record.Hashtable。Record 中得 Hashtable 中有对一个 Thread 的引用是比较奇怪的。因为那将导致这个 Thread 的实例没法释放,从而导致 Task 的实例没释放。查看 Java 应用代码发现,Thread 的实例被放入 Record 实例的静态 Hashtable 中,但是没有调用 Remove。
清单 1
双击代码全选1 2 3 4 5 6 7 8 9 10 | public class XXXXXX extends XXXXXBase
{
// …
private static Hashtable currentXXXXXXX = new Hashtable();
// …
public void process (xxxx){
// …
currentXXXXXX.put(Thread.currentThread(), XXXX_);
// …
}
|
引用 B 代码分析
和引用 A 相似,Thread 被放入了 Factory 的静态实例的 Hashtable 中,而且没有 Remove。
引用 C 代码分析
Task 是经由 Event 每次新建实例来启动执行,当执行完后应当销毁该 Task 的实例,不应长期存在于内存中。上图的应用分析显示 Event 中引用了 Task 的实例,因此 Task 没法释放。查看 Event 的代码证明了确认如此,没有将新建的 Task 实例重设为 Null。
图 11 引用分析结构图
直接用 OQL(Object Query Language) 来查询该 Task 实例,可以看到该 Task 的实例随着时间不但增多。
图 12. OQL 查询结果
综上所述,由于强引用的关系存在于静态实例中,所以 Task 的实例没法释放,最终导致了内存枯竭。Java 内存堆泄漏的问题,多发生在静态 Hashtable、Hashmap、Vector 的使用不当,还有诸如打开文件后没有关闭,DB 和 Socket 连接打开没有关闭之类的都会导致 GC 无法释放引用的 Java 实例。
本文中所描述的通过 Java 内存堆和 GC 日志来分析内存泄漏方法,以及 Eclipse MAT 和 IBM Pattern Modeling and Analysis Tool for Java Garbage Collector 工具适用于调查任何平台上的 Java 应用程序。但文中提及的截取 Java 内存堆的转存储文件方法只限于在 AIX 平台上的 IBM JDK。针对 Linux, Window 等平台,或 Sun JDK 等有专门的截取方法,不在本文中一一描述。
结束语
本文通过对一个实际内存泄漏的分析,以及一些实际使用中的工具和经验技巧的介绍,展示里分析 Java 内存分析的常规方法。