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

来源:互联网 发布:舒尔特表训练法软件 编辑:程序博客网 时间:2024/05/22 18:05

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

本文将主要面向 Java 的开发人员和性能测试人员介绍一些 AIX 平台上调查 Java 内存问题的工具以及分析方法。

0 评论:

颜 晓超, 软件工程师, IBM

顾 彬, 软件工程师, IBM

2012 年 8 月 09 日

  • +内容

在 IBM Bluemix 云平台上开发并部署您的下一个应用。

现在就开始免费试用

引言

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 内存分析的常规方法。


0 0