Dump调试分析

来源:互联网 发布:软件系统开发计划书 编辑:程序博客网 时间:2024/05/20 00:52

参考:

http://www.ibm.com/developerworks/cn/java/j-memoryanalyzer/index.html

http://chiyx.iteye.com/blog/1528782

 

1 Dump文件类型:

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*NYNNNNHPROFJava 堆大小YYYYYYYNIBM 系统转储文件Java 堆大小 + 30%YYYYYYYY

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

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

 

2 获得Dump

2.1 交互式方法

  • 使用 Ctrl+Break

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

-Xrunhprof:format=b,file=heapdump.hprof

  • 使用 jmap 工具

jmap -dump:live,format=b,file=my 2816

显示:

Dumping heap to /home/syllen/my ...
Heap dump file created

  • 使用操作系统

    使用“无害”的 gcore 命令或破坏性的 kill -6 或 kill -11 命令来生成一个内核文件。然后,使用 jmap从内核文件中提取一个堆转储文件:

        jmap -dump:format=b,file=heap.hprof path to java executable core
  • 使用 JConsole 工具:

    dumpHeap 操作是基于 JConsole 的 HotSpotDiagnostic MBean 提供的。这个操作要求必须生成一个 HPROF Dump。

  • jvisualvm工具

监视 选项卡,点击 堆Dump,即可生成,默认目录 /tmp/visualvm.dat/localhost_2816

  • MAT 工具

File -> file -> Acquire Head Dump,选择进程,堆文件存放位置

2.2 基于事件

遇到 OutOfMemoryError 时: 如果运行中应用程序设置了 -XX:+HeapDumpOnOutOfMemoryError 命令行选项,那么当出现 OutOfMemoryError 错误时就会有一个 HPROF 格式的转储文件生成。在生产系统中使用这种方法非常好,因为它几乎一直需要分析内存问题,并且它不会引起额外的性能开销。在旧版本的基于 HotSpot 的 Java 运行时中,每次 JVM 执行时这个事件所产生的堆转储文件数量并没有限制;而在新的版本中,每次 JVM 执行的事件所生成的堆转储文件具有一个最大值。


3 MAT( Memory Analyzer Tools)分析

FIle,导入生成的dump文件后,可以进行分析:

3.1 普通分析

通过 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 文件(协议域)。

3.2 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.capacityFROM java.nio.DirectByteBuffer kWHERE ((k.viewedBuffer=null)and(inbounds(k).length>1))

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

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

通过 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' 功能

3.4 线程数据使用

如 表 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.method2 的 String,而该字符串的内容 user1 是用红色圈高亮显示的。您可以想象一下,如果能够基于每一个线程堆栈结构及其对象重建或反向工程生成转储文件发生时的状态。注意,由于运行时的优化,这里并不能显示所有相关的对象,如方法参数或对象实例(虽然这些对象存在于转储文件中),但是这些对象仍然能够按通常方式进行主动 “处理”。

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

定位快照转储文件中出现的异常的一种方法是使用 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 显示的是应用程序中仍存在的所有异常,以及当异常抛出时所显示的消息。

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

虽然从转储文件找到异常对象使您能够恢复异常消息,有时异常消息太过于普通或模糊,以致您无法理解问题的根源。

 

4 总结:

4.1 一种情况

如果两个对象 A 和 B 互相之间没有直接引用,但是都外部引用某个集合的对象 C,那么 C 对象集合的 Retained Heap 将不会包含在A 或 B 占用集合中,而是包含在 A 与 B 拥用者的占用集合中。在某些情况下,B 可能会临时观察集合 C,它实际上是 A 的派生对象。在这种情况下,您可以右键单击 A ,然后选择 Java Basics > Customized Retained Set,并使用 B 的地址作为排除(-x)参数。

4.2 覆盖范围和内存效率

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

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

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

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

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

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

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

4.3 重复字符串

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

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

4.4 浪费空间的 char 数组

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