现在的位置: 首页 > 综合 > 正文

AIX 平台上基于 IBM JDK 的 Java 应用内存泄漏分析

2017年11月24日 ⁄ 综合 ⁄ 共 2949字 ⁄ 字号 评论关闭

引言

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 日志文件
图 1. 选择打开 IBM JDK 的 GC 日志文件 

图 2. 点击 Graph View Part 显示
图 2. 点击 Graph View Part 显示 

图 3. 显示 GC 分析图
图 3. 显示 GC 分析图 

从图中可以看出 Java 内存的堆 (Heap) 的使用情况是持续的上升趋势。

由此我们可以得出结论,Java 应用程序存在内存泄漏问题,导致内存堆得不到释放。

第二步,截取 Java 内存堆的转存储文件

在得出是内存堆泄漏的问题结论后,接下来就需要取得内存堆的转存储文件来做进一步分析。

在 AIX 平台上截取 IBM JDK 的内存堆的转存储文件前,需要先对 IBM JDK 的 JVM 参数进行设置。有 2 种设置方式:

  1. 设置 IBM JDK 的全局变量:

     export IBM_HEAPDUMP=true 
    

  2. 添加 JVM 启动参数:

    -Xdump:system+heap+java:events=user,request=exclusive+prepwalk+compact

    设定完后需要重启 JVM, 使设定生效。然后可以在 kill -QUIT pid 命令来生成转存储文件 (Dump),pid 为实际启动的 JVM 进程 ID。

    当内存泄漏情况非常小且缓慢的时候,无法从 1 个或 2 个转存储文件中分析出导致泄漏的 Java 对象。根据上面 GC 的日志趋势,制定如下的转存储文件的截取的方案。

    1. 截取周期为 1 星期以上,每天一次。
    2. 每天固定时间截取,且避开发生大的 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
图 4. 点击 Leak Suspect 

图 5. 显示某 1 天的转存储文件分析结果。
图 5. 显示某 1 天的转存储文件分析结果。 

如果连续几天的转存储文件中,都是这个 Suspect 实例 (Instance) 的所占比例最大,且所占内存空间也在不断上升,没有下降的趋势的话,那基本上可以断定该实例是发生泄漏的对象了。

点击打开该 Suspect 的 Detail 信息。

图 6. 点击 Details 链接
图 6. 点击 Details 链接 

通过比对连续几天的转存储文件,可以发现是 Hashtable 中得 Entry 对象的占用空间不断变大。

图 7. 显示 Detail 信息
图 7. 显示 Detail 信息 

Figure xxx. Requires a heading 

那接下来进一步深入分析,到底在 Hashtable 中占用空间增大到底是什么实例。

2)深入分析

点击 Suspect 实例,打开该实例的 Dominator Tree。

图 8. 选择 Dominator Tree 选项
图 8. 选择 Dominator Tree 选项 

可以在 Dominator Tree 中看到 Hashtable 中放的 Java Instance,依次为

Company[] -> Event[] -> Task (Manager, Handler, xxxxx)

图 9. 显示 Dominator Tree 信息
图 9. 显示 Dominator Tree 信息 

分析其中 1 个复杂的 Task,点击 Path to GC Roots 继续深入分析 Task 的引用关系。Weak 和 Soft 引用会在 Major GC 是被释放,所以查看下不包含他们的引用关系。

图 10. 显示可疑点的引用关系图
图 10. 显示可疑点的引用关系图 

根据 Java 应用的代码调查,Company 和 Event 是常驻于 Service 静态实例中。

引用 A 代码分析

引用 A 的顺序 Task <- Thread <- Record.Hashtable。Record 中得 Hashtable 中有对一个 Thread 的引用是比较奇怪的。因为那将导致这个 Thread 的实例没法释放,从而导致 Task 的实例没释放。查看 Java 应用代码发现,Thread 的实例被放入 Record 实例的静态 Hashtable 中,但是没有调用 Remove。

清单 1

				 
 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 引用分析结构图
图 11 引用分析结构图 

直接用 OQL(Object Query Language) 来查询该 Task 实例,可以看到该 Task 的实例随着时间不但增多。

图 12. OQL 查询结果
图 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 内存分析的常规方法。

http://www.ibm.com/developerworks/cn/java/j-lo-aixjvm/

抱歉!评论已关闭.