从转储(Dump)文件中调试并除错

使用 Memory Analyzer 检查内存泄漏以及更多问题

Memory Analyzer 是一个强大的分析内存泄漏和 Java™ 进程转储文件覆盖问题的工具。您也可以使用这个工具对 Java 代码执行详细分析,以及从一个转储文件中调试多个复杂问题,而不需要插入分析代码。在本文中,您将了解如何生成 Dump,以及如何使用它们来检查您的应用程序状态。

Chris Bailey, Java 支持架构师, IBM

Chris BaileyChris Bailey 是位于英国的 Hursley Park Development Lab 的 IBM Java Technology Center 团队成员。作为 IBM Java 服务和支持组织的技术架构师,他负责支持 IBM SDK for Java 用户交付成功的应用程序部署。Chris 还参与收集和评估新需求,交付新调试功能和工具,改进文档并提高 IBM SDK for Java 的质量。



Andrew Johnson, Java 工具开发人员和 Eclipse Memory Analyzer 工具代码提交者, IBM

Andrew JohnsonAndrew Johnson 是 IBM 英国赫斯利 Java 技术中心的一位特许工程师和咨询软件工程师。他是在 1988 年获得剑桥大学的电子工程学士学位后加入 IBM 的。从 1996 年开始,他参与 Java 虚拟机、实时编译器和 Java 问题分析工具等的开发。他为 Eclipse Memory Analyzer 编写了一个读取 IBM VM 的转储文件的适配器,他现在是该项目的一个代码提交者。



Kevin Grigorenko, 软件工程师,WebSphere 应用服务器 SWAT 团队, IBM

http://www.ibm.com/developerworks/i/p-kgrigorenko.jpgKevin Grigorenko 是 WebSphere 应用服务器 SWAT 团队的一名软件工程师,这个团队负责提供全球现场和远程的辅助产品缺陷支持,特别是在关键客户支持情况中。他目前专注于 WebSphere 应用服务器和相关产品的问题确定,包括 JVM 和各种操作系统。他还具有丰富的开发经验,包括 Java Enterprise Edition、C、C++、Perl、PHP、Python、Ruby 和 .NET。



2011 年 7 月 04 日

Memory Analyzer 不同版本

IBM 的 Java 监控和分析工具 — Memory Analyzer 通过使用 IBM Diagnostic Tool Framework for Java (DTFJ) 来扩展 Eclipse MAT 1.0 而将 Eclipse Memory Analyzer Tool (MAT) 的分析功能引入到 IBM 的 Java 虚拟机上。DTFJ 使我们能够使用操作系统级转储文件和 IBM Portable Heap 转储文件对 Java 堆进行分析。IBM 的版本属于 IBM Support Assistant (ISA)。DTFJ 插件位于独立的 Eclipse MAT 上。参考资料 中有下载链接。

通过在代码中添加调试语句来输出对象的域或者甚至于整个数据集合是一种常用的解决问题的方法。当您发现必须获得更多的信息才能够理解和解决这个问题时,通常您必须一次次地重复这个操作。虽然这个过程可能是有效的,但是有时候它也无法得到结果:调试代码的数量可能会使问题消失,您可能需要增加一些不属于您的调试代码,这样调试过程可能要求您重启进程,或者调试的总体性能影响可能使应用程序无法运行。

Memory Analyzer 是一个跨平台的开源工具,您不仅可以用它来分析内存问题,也可以用来监控整个 Java 应用程序的状态和行为。通过读取应用程序运行时由 Java 运行时环境生成的转储文件快照,Memory Analyzer 使您能够分析那些调试代码可能无法发现的复杂问题。

本文将介绍如何生成转储文件,以及如何使用它们来检查和分析应用程序的状态。通过 Memory Analyzer,您可以检查线程、对象、包和整个数据集合,以便发现导致内存泄漏的 Java 代码问题。

快照转储文件类型

Memory Analyzer 目前支持三种转储文件类型:

  • IBM Portable Heap Dump (PHD): 这个专有的 IBM 格式只包含进程中每个 Java 对象的类型和大小,以及这些对象之间的关系。这个转储文件格式远远小于其他格式,并且只包含最少的信息。但是,这些数据通常对于分析内存泄漏和了解应用程序基本架构和范围而言是足够的。
  • HPROF 二进制转储文件: HPROF 二进制转储文件在 IBM PHD 格式中包含了所有数据表现方式,以及 Java 对象和线程内部的基本数据类型,您可以查看对象中域的值,查看在转储文件产生时有哪些方法在被执行。其他基本数据使 HPROF 转储文件明显比 PHD 格式的转储文件要大;它们大约与所使用的 Java 堆一样大。
  • IBM 系统转储文件: 当使用 IBM Java 运行时环境时,原生的操作系统转储文件 — 一个 AIX® 或 Linux 的内核文件、一个 Windows® 的微转储文件或者一个 z/OS® 的 SVC 转储文件— 可能会被加载到 Memory Analyzer。这些转储文件包含了运行中应用程序的完整内存镜像 — 所有信息和数据都采用 HPROF 格式表示,包括所有原生内存和线程信息。这是最大和最全面的转储文件文件格式。

这两种 IBM 转换文件类型都只存在于所安装的 Diagnostic Tool Framework for Java (DTFJ)(见 参考资料Memory Analyzer 不同版本 侧栏内容)。

表 1 总结了这些转储文件类型的区别:

表 1. 转储类型的特点总结
转储文件格式近似磁盘大小对象、类和类加载器线程信息域名称域和数据引用基本域基本数组内容精确的垃圾收集源原生内存和线程
IBM PHD20 % 的 Java 堆大小Y具有 Javacore*NYNNNN
HPROFJava 堆大小YYYYYYYN
IBM 系统转储文件Java 堆大小 + 30%YYYYYYYY

* 通过加载 javacore.txt 文件(IBM 线程转储文件)和同时产生的 heapdump.phd 文件,Memory Analyzer 在 IBM PHD 格式的转储文件中记录线程信息。

HPROF 和 IBM 转储文件格式都可以通过操作系统工具进行充分压缩,通常能够压缩到它们原始大小的 20%。


获得快照转储文件

获取每个 Java 运行时的各种转储文件需要使用不同的机制,它们能够让您灵活地生成各种情况的转储文件,包括 OutOfMemoryError。这些机制取决于您使用的是哪个供应商的 Java 运行时环境。

先决条件

对于所有类型的转储文件,您必须保证预留足够的转储文件磁盘空间,这样它们才不会被删减。转储文件的默认位置是 JVM 进程的当前工作目录。对于 IBM JVM,您可以通过 -Xdump 文件命令行选项修改这个位置。对于 HotSpot JVM,您可以使用 -XX:HeapDumpPath 命令行选项进行修改。详见 参考资料 中关于相关语法的链接。

操作系统的转储文件可用于 IBM 和 HotSpot JVM。对于 IBM JVM,您可以使用 jextract 工具(JDK 自带)来创建转储文件,并将它们直接加载到 Memory Analyzer 中;对于 HotSpot JVM,您可以使用 jmap 工具从内核转储文件中提取堆 Dump。(我们将在文章后面内容中讨论这两种技术。)然而,在一些操作系统上,您必须保证这个进程在创建内核转储文件之前具有足够的 ulimit。如果 ulimit 不正确,那么您必须修改它们,并重新启动这个进程,才能够收集一个转储文件。请查看 参考资料 中关于获取 AIX、Linux®、z/OS 和 Solaris 系统转储文件的详细信息的链接。

获取一个快照转储文件:HotSpot 运行时环境

基于 HotSpot 的 Java 运行时只能够生成 HPROF 格式的转储文件。您可以选择以下几种交互方法和一种基于事件的方法来生成这个转储文件:

  • 交互式方法:
    • 使用 Ctrl+Break:如果运行的应用程序设置了 -XX:+HeapDumpOnCtrlBreak 命令行选项,那么在通过控制台发出 Ctrl+Break 事件或 SIGQUIT(通常通过 kill -3 生成),那么会生成一个 HPROF 格式的转储文件和一个线程 Dump。有一些版本不支持这个选项,那么在遇到这些情况时可以尝试使用:

      -Xrunhprof:format=b,file=heapdump.hprof
    • 使用 jmap 工具jmap 实用工具(见 参考资料)位于 JDK 的 bin 目录,它提供了一种从运行中的进程请求一个 HPROF 转储文件的选项。在 Java 5 中,要使用:

      jmap -dump:format=b pid

      而在 Java 6 中,要使用以下命令,其中 live 是可选的,表示只返回正在写到转储文件进程 ID (PID) 的 “live” 对象:

      jmap -dump[live,]format=b,file=filename pid
    • 使用操作系统:使用“无害”的 gcore 命令或破坏性的 kill -6kill -11 命令来生成一个内核文件。然后,使用 jmap 从内核文件中提取一个堆转储文件:

      jmap -dump:format=b,file=heap.hprof path to java executable core
    • 使用 JConsole 工具:dumpHeap 操作是基于 JConsole 的 HotSpotDiagnostic MBean 提供的。这个操作要求必须生成一个 HPROF Dump。
  • 基于事件的方法:
    • 遇到 OutOfMemoryError 时: 如果运行中应用程序设置了 -XX:+HeapDumpOnOutOfMemoryError 命令行选项,那么当出现 OutOfMemoryError 错误时就会有一个 HPROF 格式的转储文件生成。在生产系统中使用这种方法非常好,因为它几乎一直需要分析内存问题,并且它不会引起额外的性能开销。在旧版本的基于 HotSpot 的 Java 运行时中,每次 JVM 执行时这个事件所产生的堆转储文件数量并没有限制;而在新的版本中,每次 JVM 执行的事件所生成的堆转储文件具有一个最大值。

获取一个快照转储文件:IBM 运行时环境

IBM 运行时环境提供了转储文件和跟踪引擎,它们可以在众多交互式和基于事件的场景中生成 PHD 格式或系统转储文件。您也可以使用 Health Center 工具或使用 Java API 编程生成交互式转储文件。

  • 交互式方法
    • 使用 SIGQUIT 或 Ctrl+Break: 当向 IBM 运行时环境发送 Ctrl+Break 或 SIGQUIT(通常是使用 kill -3 生成)时,IBM 转储文件引擎中会生成一个用户事件。默认情况下,这个事件只会生成一个线程转储文件(javacore.txt)。您可以使用 -Xdump:heap:events=user 选项来生成 PHD 格式的转储文件,或者使用 -Xdump:system:events=user 选项生成一个 Java 应用程序的系统转储文件。

    • 通过操作系统生成一个系统转储文件:

      • AIX:gencore(或者破坏性 kill -6kill -11
      • Linux/Solaris:gcore(或者破坏性 kill -6kill -11
      • Windows:userdump.exe
      • z/OS:SVCDUMP 或者控制台转储文件
    • 使用 IBM Monitoring and Diagnostics Tools for Java - Health Center: Health Center 工具提供了一个菜单选项,可用来请求从一个运行的 Java 进程生成一个 PHD 或系统转储文件(见 参考资料)。

  • 基于事件的方法。IBM 转储文件和跟踪引擎提供了灵活的功能集,它们可以根据由所执行方法抛出的大量事件来生成 PHD 和系统 Dump。通过使用这些功能,您就能够为您希望分析的大部分问题场景生成转储文件。
    • 使用 IBM 转储文件引擎: 转储文件引擎提供了大量您可用来生成一个 PHD 或系统转储文件的事件。而且,您可以用它来过滤这些事件类型,以便在生成转储文件时执行精细的控制。

      您可以通过 -Xdump:what 选项查看默认事件。例如,您会注意到,JVM 的前 4 个 OutOfMemoryError 异常会产生 heapdump.phd 和 javacore.txt 文件。

      为了收集更多的数据,您可以在 OutOfMemoryError 异常上生成一个系统转储文件,而非堆转储文件。

      -Xdump:heap:none -Xdump:java+system:events=systhrow,
       filter=java/lang/OutOfMemoryError,range=1..4,request=exclusive+compact+prepwalk

      有一些异常,如 NullPointerException,在大多数应用程序中可能会在许多处代码中发生,这使得它很难针对特定的 NullPointerException 生成转储文件。为了帮助您更明确转储文件所对应的异常,它在 “throw” 和 “catch” 事件上提供了额外的过滤机制,允许您分别指定抛出和捕捉方法。例如,下面这个选项会在 bad() 方法抛出一个 NullPointerException 异常时生成一个系统转储文件:

      -Xdump:system:events=throw,
             filter=java/lang/NullPointerException#com/ibm/example/Example.bad

      catch() 方法捕捉到一个 NullPointerException 异常时,这个选项会产生一个系统转储文件:

      -Xdump:system:events=catch,
             filter=java/lang/NullPointerException#com/ibm/example/Example.catch

      除了事件过滤之外,您还可以指定希望生成转储文件的事件范围。例如,下面这个选项只会在第五个 NullPointerException 异常上产生转储文件:

      -Xdump:system:events=throw, filter=java/lang/NullPointerException,range=5

      下面这个选项只会在第二、第三和第四个 NullPointerException 异常上产生转储文件:

      -Xdump:system:events=throw, filter=java/lang/NullPointerException,range=2..4

      表 2 列出了最有用的事件和过滤器:


      表 2. 可用的转储文件事件
      事件描述可用过滤示例
      gpf一般保护故障(崩溃)-Xdump:system:events=gpf
      user用户触发信息(SIGQUIT 或 Ctrl+Break)-Xdump:system:events=user
      vmstopVM 关闭,包括调用 System.exit()退出代码-Xdump:system:events=vmstop,filter=#0..#10
      在 VM 关闭时生成带有 010 退出代码的系统转储文件。
      load类加载类名称-Xdump:system:events=load,filter=com/ibm/example/Example
      com.ibm.example.Example 类加载时生成一个系统转储文件。
      unload类卸载类名称-Xdump:system:events=unload,filter=com/ibm/example/Example
      com.ibm.example.Example 类卸载时生成一个系统转储文件。
      throw抛出一个异常异常类名-Xdump:system:events=throw,filter=java/net/ConnectException
      ConnectException 产生时生成一个系统转储文件。
      catch捕捉到一个异常异常类名-Xdump:system:events=catch,filter=java/net/ConnectException
      ConnectException 被捕捉时生成一个系统转储文件。
      systhrowJVM 将抛出一个 Java 异常。(这与 throw 事件不同,因为它只能由 JVM 内部检测到的错误条件触发。)异常类名-Xdump:system:events=systhrow,filter=java/lang/OutOfMemoryError
      OutOfMemoryError 产生时生成一个系统转储文件。
      allocation分配一个 Java 对象所分配对象的大小-Xdump:system:events=allocate,filter=#5m
      当分配大于 5MB 的对象时生成一个系统转储文件。
    • 使用 IBM 跟踪引擎:这个跟踪引擎允许 PHD 和系统转储文件由应用程序中任何 Java 方法进入或退出事件触发。您可以在控制 IBM 跟踪引擎的 -Xtrace 命令行选项中使用 trigger 关键字来实现这个配置。这个触发器选项的语法是:
      method{methods[,entryAction[,exitAction[,delayCount[,matchcount]]]]}

      将下面的命令行选项添加到应用程序上会在 Example.trigger() 方法被调用时产生一个系统转储文件:

      -Xtrace:maximal=mt,trigger=method{com/ibm/example/Example.trigger,sysdump}

      这个命令行选项会在 Example.trigger() 方法被调用时产生一个 PHD 转储文件:

      -Xtrace:maximal=mt,trigger=method{com/ibm/example/Example.trigger,heapdump}

      但是,推荐您设置一个范围,这样您就不会在每次这个方法被调用时都产生转储文件。下面这个例子会忽略 Example.trigger() 的前五次调用,然后触发产生一个转储文件:

      -Xtrace:maximal=mt,trigger=method{com/ibm/example/Example.trigger,sysdump,,5,1}

      注意这个例子中的 exitAction 使用了一个空项,因为我们只触发该方法的进入事件。
  • 通过编程方法实现:IBM 运行时环境还提供了一个 com.ibm.jvm.Dump 类及其 javaDump()、heapDump()systemDump() 方法。它们分别产生线程转储文件、PHD 转储文件和系统转储文件。

通过 Memory Analyzer 获取一个转储文件

除了运行时环境本身所提供的获取转储文件的方法,Memory Analyzer 还提供了一个 Acquire Heap Dump 选项,如图 1 所示,它允许您触发并从一个与 Memory Analyzer 运行在同一个主机上的 Java 进程加载一个快照转储文件。

图 1. 使用 Memory Analyzer 中的 Acquire Heap Dump 功能
截图显示 Memory Analyzer 中的 Acquire Heap Dump 功能

在基于 HotSpot 的运行时环境中,Memory Analyzer 会使用 jmap 生成这个转储文件。对于 IBM 运行时环境,转储文件是使用 Java 的 “后附加” 功能和编程 API 生成的。这个功能要求使用 Java 6SR6 ,因为之前的版本并不包含 “后附加” 功能。

后续处理需求

对于 IBM 系统转储文件,这个转储文件必须使用 JDK 自带工具 jextract 进行后处理:

jextract core

理想情况下,jextract 运行在产生该转储文件的物理主机上,它使用生成该转储文件的 JDK 的 jextract,并且能够读取 java 进程所运行的相同程序库。由于 jextract 可能会占用大量的 CPU 时间来处理转储文件,所以这在一些生产系统上是不可行的。在这种情况下,转储文件应该在最近的符合要求的系统上进行处理,如生产前的测试系统。Java 运行时环境的 Service Refresh (SR) 和 Fix Pack (FP) 应该符合要求。

jextract 会产生一个 ZIP 文件,它包含了原始的内核转储文件、转储文件的一个处理后表示、Java 可执行文件和 java 进程所使用的库文件。在运行 jextract 之后,您可以删除这个原始(未压缩)内核转储文件。而这个 ZIP 文件就是您需要加载到 Memory Analyzer 中的文件。

通过将 ZIP 文件加载到 jdmpview,并执行 heapdump 命令(见 参考资料),您就可以从一个 jextract 处理后的系统转储文件中提取一个 PHD 转储文件。


使用 Memory Analyzer 分析问题

通过查看有内存泄漏或范围需求大于可用内存的应用程序的相关方面,Memory Analyzer 就能够检测到 OutOfMemoryError。Memory Analyzer 会自动执行内存泄漏检查,并生成一个 Leak Suspects 报告(见 参考资料)。

HPROF 和 IBM 系统转储文件所提供的额外数据,特别是域名称和域值 — 以及 Inspector 视图和 Object Query Language (OQL) 功能 — 也使它能够检测更大范围的问题类型,而不只是 “谁把内存都用光了?”。例如,您可以确定集合的占用因数和负载因数,以分析它们的规模是否合理,或者查看与 ConnectException 相关的主机名和端口号,以分析应用程序尝试创建什么样的连接。

通过 Inspector 查看对象的域

一旦 Memory Analyzer 选择了对象,那么 Inspector 视图就会显示与该对象相关的可用信息,包括类层次、属性和静态变量。Attributes 面板会显示与对象相关的实例域和值,而 Statics 面板会显示与类相关的静态域和值。

图 2 所示的 Inspector 视图是一个简单的 java.net.URL 对象,您可以看到这个对象的详细信息,包括 URL 的来源与目标的协议类型:

图 2. Inspector 视图中的 Statics、Attributes 和 Class Hierarchy 面板
截图显示了 Inspector 视图的三个面板

图 2 中,您可以从 Attributes 面板看到 URL 对象引用了一个位于本地文件系统(位置由 path 和 file 域指定)的 JAR 文件(协议域)。

使用 OQL 执行对象查询

OQL 可通过定制的类似 SQL 查询语言查询转储文件。这个主题并不属于本文讨论范围,所以我们只是概括介绍一些例子。更详细的信息,请查看 Memory Analyzer 提供的 OQL 帮助文档。

OQL 特别适用于根据一组对象的外部引用和域来查找特定的域。例如,如果类 A 具有一个 B 类型的域 foo,而类 B 具有一个 String 类型的域 bar,那么一个查询所有这些 String 的简单查询可以是:

SELECT aliasA.foo.bar.toString()
FROM A aliasA

我们给类 A 创建一个别名 aliasA,然后我们会在 SELECT 子句中引用这个别名。这个查询只查询类 A 的实例。如果我们希望查询 A 类及其所有子类的所有实例,那么我们可以使用:

SELECT aliasA.foo.bar.toString()
FROM INSTANCEOF A aliasA

下面是 DirectByteBuffer 的一个更复杂的例子:

SELECT k, k.capacity
FROM java.nio.DirectByteBuffer k
WHERE ((k.viewedBuffer=null)and(inbounds(k).length>1))

在这个例子中,我们希望获得所有 DirectByteBuffer 的功能域,因为它包含了这个对象所占用的原生内存。我们还希望过滤所有的 DirectByteBuffer,所以我们有一个 null 值的 viewedBuffer 域(因为这些只是其他 DirectByteBuffer 的视图)和一个以上的入站引用(所以我们会忽略这些虚引用的未完清理操作 — 即,我们只希望获得 “激活的” DirectByteBuffer)。

对视图与转储文件进行比较

通过 Memory Analyzer,您可以对比这些查询生成的表。这些表可能是来自于同一个转储文件,您可以从中看到来自同一个视图的 String 对象是否位于另一个视图的某个集合对象中,它们也可能来自不同的转储文件,您可以从中看到数据变化,例如对象集合的增长。

为了执行一个比较,您需要将相关的表添加到 Compare Basket 中,然后请求其中的记录。首先是查找 Navigation History 中表的记录,然后选择快捷菜单中的 Add to Compare Basket,如图 3 所示:

图 3. 将来自 Navigation History 视图的表添加到 Compare Basket
截图显示 Memory Analyzer 中的 'Add to Compare Basket' 功能

当您的 Compare Basket 具有两个记录时,您就可以使用面板右上角的 Compare-the-results 按钮(红色感叹号)执行比较,如图 4 所示:

图 4. 比较 Compare Basket 中记录的结果
截图显示了 Memory Analyzer 中的 'Compare the results' 功能

覆盖范围和内存效率

Memory Analyzer 的另一个重要用途是查询占用堆最多的组件有哪些,即使没有出现内存泄漏现象。如果内存使用减少,那么系统容量或性能就能够得到提升,从而允许增加会话或减少垃圾收集时间。

Top Components 报告是第一步。它将系统的内存使用按组件进行划分,分析每一个组件的内存使用情况,并且会查找浪费的情况。由其他对象占用(保持)的对象可以称为由该占用者 拥有。Top Components 报告列举了另一个对象所拥有的所有对象。它们是堆的最大占用者。然后它们会按照使用这些对象类的类加载器进行划分,而所有这些最大占用者及其拥有的对象会被分配到对应的类加载器上。您可以选择报告中的类加载器进行深入的分析,以便打开一个新的类加载器特有的组件报告。

对于每一个组件,其集合对象都会被分析。集合类,如 java.util.* 所示,都是程序员可以节省大量执行时间的对象,因为它们实现了经过良好测试的列表、集合和图。一般的应用程序可能有上百万个集合,所以在集合中浪费的空间是巨大的。

空集合是一个常见的内存浪费原因。ArrayListVectorHashMapHashSet 在创建时会使用默认大小的后备数组,可能支持保持 10 条记录,以准备保存记录。而在一些应用程序中创建了一个集合而不用它来保存任何对象的情况是非常常见的。这会快速地消耗内存。例如,100,000 个空集合,其备用数组可能消耗 100,000 * (24+10*4) 字节 = 6 MB。

Empty Collection 报告会检查标准的集合类及其扩展类,并分析它们的大小。然后它会为每个集合生成一个表,并按照集合大小排序,其中最频繁使用的大小排在前面。如果有某种类型的集合有大量的实例是空的,那么报告会将它标记为可能的内存浪费。

其中一种解决方法是延迟集合的内存分配,在有记录插入该集合时才分配内存。另一个方法是给集合分配一个 0 或 1 的默认大小,而在它需要时动态地增加。第三个方法是在初始化完成之后压缩集合的大小。

有一个相关的问题是只有少量记录而有大量浪费空间的集合。Collection Fill Ratio 部分显示了每一种集合类型,该集合的在特定填充比例下的实例数量。这可以帮我们发现具有大量空白空间的集合。

重复字符串

字符串和字符数组在典型的业务应用程序中占用了大量的空间,所以它们是另一个值得分析的方面。这部分的组件报告会分析常见内容的字符串。字符串是不可修改的。具有相同值的字符串常量是由 VM 规范保证使用同一个实例的。动态创建的字符串是没有这样的保证的,例如,从数据库或磁盘读取的相同值数据所创建的两个 String 会使用不同的实例和独立的底层字符数组。如果这些字符串都保存下来,那么这可能会造成很严重的空间浪费。

您可以通过使用 String.intern() 或维护一个用户散列集合或散列映射来解决这个问题。

浪费空间的 char 数组

String.substring() 在 Java 语言中是通过创建一个共享原始字符数组的新 String 而实现的。如果原始字符串一直被使用,那么这种方法是很高效的。但是如果只有一小段字符串被使用,那么 — 由于整个字符数组仍然被保存 — 就有一些空间被浪费了。Waste In Char Arrays 查询能够显示字符串所引用的字符数组中浪费的空间。


Eclipse 包和类加载器层次结构

现代应用程序是按组件划分的,它们通常是基于类加载器的,以便在应用程序各部分实现一定的隔离。组件可以通过停止某个组件类加载器的使用和使用新的类加载器加载新版本组件而进行更新。最后,如果应用程序没有任何引用类、对象或类加载器,旧版本组件会被垃圾收集器释放。

Class Loader Explorer 查询会显示系统的所有类加载器,所以它支持所有的应用程序。它会显示类加载器所加载的类,以及类加载器的上级类层次,这样我们就能够发现类加载问题。通过检查,您可以确定类加载器是否有多个副本。如果类加载器几乎没有定义任何的类实例,那么这个类加载器很可能是空闲的。

经常使用的一个类加载框架是 OSGi 框架。它的其中一种实现是 Eclipse Equinox,基于 Eclipse 的应用程序可用它来分隔插件,此外 WebSphere® Application Server 6.1 及以上版本也使用了这个框架。当尝试理解应用程序的状态时,了解所有包的状态是非常有用的。Eclipse Equinox Bundle Explorer 查询正是执行这个操作的工具,如图 5 所示:

图 5. Eclipse Bundle Explorer
截图显示的是 Bundle Explorer 视图

系统或 HPROF Dump 包含所有的对象和域。Bundle Explorer 显示了系统的所有包及其状态、依赖程序和服务。它能够显示不必要的包,因此允许您关闭这些包以使用更多的资源。


线程数据使用

表 1 所示,一个转储文件可以包含线程的一些详细信息,它们有助于我们理解转储文件生成时所发生的问题。这可能包含所有激活的线程堆,每个线程的所有帧,而且最重要的是,还包括这些帧中激活的部分或全部的 Java 局部变量。

Thread Overview 视图

如图 6 所示,Thread Overview 视图显示了 JVM 的所有线程,以及该线程的所有属性,如它占用的堆大小、上下文类加载器、优先级、状态和原生 ID:

图 6. Thread Overview
截图显示的是 Thread Overview 视图

如果在 OutOfMemoryError 中实际上没有出现任何 Java 堆问题,但是这些线程所占用堆的总数又的确 “太大了”,那么这个占用的堆大小信息是非常有用的。在这种情况中,JVM 的设置大小可能不够大,而线程池大小可能太大了,或者某个线程的 Java 堆的平均或最大 “负载” 太高了。

Thread Stacks 视图

如图 7 所示的 Thread Stacks 视图,显示了所有的线程、线程的堆、堆栈结构及这些堆栈结构中的 Java 局部变量:

图 7. Thread Stacks 视图
截图显示的是 Thread Stacks 视图

Thread Details 视图

在 Thread Overview 和 Thread Stacks 视图中,您可以右键单击一个线程,然后在菜单顶部或通过 Java Basics > Thread Details 选择 Thread Details。这个视图会显示更详细的信息,如原生堆信息(如果有)。

图 7 所示的例子中,有一个类型为 java.lang.Thread 的线程 main — 一个简单的命令行程序的主线程 — 被展开。这个线程的每一个堆栈结构都显示在视图中,而那些可用的 Java Locals 也是可以展开的。在这里是作为参数从 Play.method1 传递给 Play.method2String,而该字符串的内容 user1 是用红色圈高亮显示的。您可以想象一下,如果能够基于每一个线程堆栈结构及其对象重建或反向工程生成转储文件发生时的状态。

注意,由于运行时的优化,这里并不能显示所有相关的对象,如方法参数或对象实例(虽然这些对象存在于转储文件中),但是这些对象仍然能够按通常方式进行主动 “处理”。


异常分析

当应用程序产生异常时,复杂性的增加会使得异常原因的分析难度加大。下面是两个此类问题的实例:

  • 使用日志机制意味着会丢失异常或异常消息。
  • 这个异常产生的消息所包含的信息量不足。

在第一种情况中,异常消息或整个异常可能会完全丢失,这使我们很难发现问题或者了解问题的基本信息。在第二种情况中,这个异常被记录下来,并且可以看到异常消息,也能够跟踪堆,但是它并不包含足够发现异常根源的必要信息。

因为 Memory Analyzer 能够访问对象内部的域,所以我们可能从异常对象中发现异常消息。在一些情况中,我们还可能提取不在原始异常中的其他数据。

在快照转储文件中定位异常

定位快照转储文件中出现的异常的一种方法是使用 Memory Analyzer 的 OQL 功能来定义与转储文件相关的对象。例如,下面这个查询会查询所有的异常对象:

SELECT * 
FROM INSTANCEOF java.lang.Exception exceptions

下一个查询会生成一个异常清单,其中您可以使用 Inspector 视图来查看每一个异常的域。如果您知道包含异常消息的域是 detailMessage 域,那么您还可以修改查询以直接获取这个异常消息,并马上将它们显示在结果表中:

SELECT exceptions.@displayName, exceptions.detailMessage.toString() 
FROM INSTANCEOF java.lang.Exception exceptions

前一个查询会生成如图 8 所示的输出结果:

图 8. OQL 异常查询的输出,包括异常消息
截图显示的是 OQL 查询的输出

图 8 显示的是应用程序中仍存在的所有异常,以及当异常抛出时所显示的消息。

提取与异常相关的额外信息

虽然从转储文件找到异常对象使您能够恢复异常消息,有时异常消息太过于普通或模糊,以致您无法理解问题的根源。其中一个很好的例子是 java.net.ConnectException。如果尝试创建一个套接字连接来访问一个无法连接的主机时,您会得到下面的消息:

java.net.ConnectException: Connection refused: connect
     at java.net.PlainSocketImpl.socketConnect(Native Method)
     at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:352)
     at java.net.PlainSocketImpl.connectToAddress(PlainSocketImpl.java:214)
     at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:201)
     at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:377)

     at java.net.Socket.connect(Socket.java:530)
     at java.net.Socket.connect(Socket.java:480)
     at java.net.Socket.(Socket.java:377)
     at java.net.Socket.(Socket.java:220)

如果您有创建这个套接字的代码,并且您能够看到代码所使用的主机名和端口,那么这个消息就已经足够。在更复杂的代码中,其主机名和端口会经常变化,因为它们是从外部来源获取(用户输入值、数据库等),那么这个消息可能无法帮助您理解为什么连接被拒绝。

堆跟踪应该包括一个套接字对象,其中包含了所需要的数据,而如果我们能够使用 Memory Analyzer 从快照转储文件中查找这个套接字对象,然后我们就能够确定连接所拒绝的主机名和端口。

完成这个操作的最简单方法是在异常抛出后就生成一个转储文件。这在 IBM 运行时环境中可以使用以下 -Xdump 选项集实现:

-Xdump:system:events=throw,range=1..1,
       filter=java/net/ConnectException#java/net/PlainSocketImpl.socketConnect

这个选项会在 PlainSocketImpl.socketConnect() 方法第一次出现 ConnectException 时生成一个 IBM 系统转储文件。

在将所生成的转储文件加载到 Memory Analyzer 之后,我们可以使用 Open Query Browser > Java Basics > Thread Stacks 选项列出线程跟踪中与每一个方法相关的线程和对象。

通过展开当前线程和线程中的方法帧,您就能够查看与这些方法相关的对象。在遇到 java.net.ConnectException 时,最应该关注的方法是 java.net.Socket.connect()。展开这个方法帧会显示内存中一个 java.net.Socket 对象的引用。这是我们尝试创建的套接字连接。

Socket 对象选中时,它的域会显示在 Inspector 视图中,如图 9 所示:

图 9. Socket 对象的 Inspector 视图
截图显示一个 Socket 对象的 Inspector 视图

图 9 中的信息并不是非常有用,因为 Socket 的实际实现 位于 impl 域。您可以通过下面两种方法来检查 impl 对象的内容,一是展开 Socket 对象,然后选择主面板中的 impl java.net.SocksSocketImpl,二是右键单击 Inspector 视图中的 impl 域,然后选择 Go Into。现在 Inspector 视图会显示 SocksSocketImpl 的域,如图 10 所示:

图 10. SocksSocketImpl 对象的 Inspector 视图
截图显示了一个 Socket Impl 对象的 Inspector 视图

图 10 所示的视图能够查看 addressport 域。在这里,端口是 100,但是地址域指向一个 java.net.Inet4Address 对象。按照相同的过程来查看 Inet4Address 对象的域的结果如图 11 所示:

图 11. Inet4Address 对象的 Inspector 视图
截图显示的是 Inet4Address 对象的 Inspector 视图

您会发现 hostName 被设置为 baileyt60p


技巧与方法

下面是一些有用的技巧和方法:

  • 要注意 Memory Analyzer 本身可能会遇到内存耗尽问题。对于 Eclipse MAT,要编辑 MemoryAnalyzer.ini 文件中的 -Xmx 配置值。对于 ISA 版本,要编辑 ISA install/rcp/eclipse/plugins/com.ibm.rcp.j2se.../jvm.properties 文件。
  • 如果您的 32 位 Memory Analyzer 仍然遇到内存耗尽问题,可以改用 64 位 Eclipse MAT 或尝试使用傻瓜(headless)模式(见 参考资料)。(ISA 工具目前不支持 64 位平台。)
  • Memory Analyzer 会在转储文件的目录中写入 “交换” 文件,这会减少转储文件的重新加载时间。这些可以经过压缩,发送到另一台主机,然后保存到转储文件的相同目录中,这样就不需要重新加载完整的转储文件。
  • 如果转储文件的大小在转储文件发生时与垃圾收集器不匹配,那么要查看 Overview 选项卡中的 Unreachable Objects Histogram 链接。Java 堆可能会有很多的垃圾(例如,如果很长时间之前创建的集合有一段时间未使用)需要 Memory Analyzer 删除。
  • 如果两个对象 AB 互相之间没有直接引用,但是都外部引用某个集合的对象 C,那么 C 对象集合的 Retained Heap 将不会包含在 AB 占用集合中,而是包含在 AB 拥用者的占用集合中。在某些情况下,B 可能会临时观察集合 C,它实际上是 A 的派生对象。在这种情况下,您可以右键单击 A,然后选择 Java Basics > Customized Retained Set,并使用 B 的地址作为排除(-x)参数。
  • 您可以一次加载多个转储文件,然后进行比较。打开较新转储文件的 Histogram,单击顶部的 Compare,然后选择基线转储文件。
  • 当您浏览一个引用树时,一定要知道这些引用可以直接或间接地指回一个 “父” 引用,这样您可以输入一个浏览循环或环路(例如,链表)。一定要知道对象的地址。此外,一定要知道如果对象的类名之前加上关键词 class,那么您浏览的是这个类的静态实例。
  • 大多数视图中显示的 String 值最多可以有 1,024 个字符。如果您使用整个 String,那么您可以右键单击该对象。然后选择 Copy > Save value to file
  • 大多数视图都有一个导出按钮,而大多数 HTML 结果都是在文件系统中创建的,所以数据可以导出共享或进行进一步的转换。与之相关的是,您可以使用组合键 Ctrl+C 将表格中选中的任意行以文本方式复制到您的剪贴板中。

正如 Eclipse.org 所描述的,Memory Analyzer 一开始是作为 “一个帮助您查询内存泄漏和减少内存消耗的快速富特性 Java 堆分析器” 而开发的。但是它的功能显然超越了所描述的范畴。除了具有分析 “常见的” 内存问题的作用,快照转储文件还可以作为其他判断问题技术的替代或补充,如跟踪技术和补丁技术。特别是对于 HPROF Dump 和 IBM 系统转储文件,Memory Analyzer 能够给您许多内存信息,如原始源代码使用的基本数据类型和域的名称。通过使用本文所介绍的各种视图,您可以查看遇到的问题,或者对它进行反向工程,包括总体覆盖问题和内存效率问题、Eclipse 包和类加载器的关系、线程数据使用率和堆栈结果局部变量、异常等。OQL 和 Memory Analyzer 插件模型也使您能够更容易地使用查询语言和编程方法来检查转储文件,这有助于实现常见分析的自动化。

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=696972
ArticleTitle=从转储(Dump)文件中调试并除错
publish-date=07042011