The NoteBook of EricKong

  BlogJava :: 首页 :: 联系 :: 聚合  :: 管理
  611 Posts :: 1 Stories :: 190 Comments :: 0 Trackbacks

引言

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

posted on 2014-05-13 19:08 Eric_jiang 阅读(282) 评论(0)  编辑  收藏 所属分类: WebShpere

只有注册用户登录后才能发表评论。


网站导航: