了解 Java 堆转储

Posted

技术标签:

【中文标题】了解 Java 堆转储【英文标题】:Understanding Java Heap dump 【发布时间】:2011-10-09 00:17:47 【问题描述】:

一周以来,我一直试图在我的应用程序中查找内存泄漏,但没有任何成功。我尝试进行堆转储并使用 jhat 查看转储并追踪内存泄漏。

这是最好的方法吗?使用堆转储跟踪内存泄漏的最佳方法是什么。

感谢您的帮助。

使用的虚拟机: java 版本“1.6.0_25” Java(TM) SE 运行时环境 (build 1.6.0_25-b06) Java HotSpot(TM) 64 位服务器 VM(内部版本 20.0-b11,混合模式)

JVM 选项: -Xmx1600m -XX:+UseParallelGC -XX:MaxPermSize=256m -Xms1600m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -verbose:gc -Xloggc: /tmp/gc.log

OOME 堆栈跟踪: 无法获取此信息。内核因内存不足错误而终止进程。

GC 日志:最后几行

48587.245: [GC [PSYoungGen: 407168K->37504K(476160K)] 506729K->137065K(1568448K), 3.0673560 secs] [Times: user=3.53 sys=0.00, real=3.07 secs] 
50318.617: [GC [PSYoungGen: 444224K->37536K(476416K)] 543785K->175177K(1568704K), 3.6635990 secs] [Times: user=3.70 sys=0.00, real=3.67 secs] 
50453.841: [GC [PSYoungGen: 70092K->2912K(476672K)] 207734K->178513K(1568960K), 1.0164250 secs] [Times: user=1.29 sys=0.00, real=1.02 secs] 
50454.858: [Full GC (System) [PSYoungGen: 2912K->0K(476672K)] [PSOldGen: 175601K->137776K(1092288K)] 178513K->137776K(1568960K) [PSPermGen: 60627K->60627K(74368K)], 2.0082140 secs] [Times: user=2.09 sys=0.00, real=2.01 secs] 
52186.496: [GC [PSYoungGen: 407104K->37312K(444416K)] 544880K->175088K(1536704K), 3.3705440 secs] [Times: user=3.93 sys=0.00, real=3.37 secs] 
53919.975: [GC [PSYoungGen: 444416K->37536K(476608K)] 582192K->213032K(1568896K), 3.4242980 secs] [Times: user=4.09 sys=0.00, real=3.42 secs] 
54056.872: [GC [PSYoungGen: 70113K->2880K(476480K)] 245609K->216320K(1568768K), 0.9691980 secs] [Times: user=1.19 sys=0.00, real=0.97 secs] 
54057.842: [Full GC (System) [PSYoungGen: 2880K->0K(476480K)] [PSOldGen: 213440K->99561K(1092288K)] 216320K->99561K(1568768K) [PSPermGen: 60628K->60628K(72320K)], 2.2203320 secs] [Times: user=2.23 sys=0.01, real=2.22 secs] 
55796.688: [GC [PSYoungGen: 406976K->37504K(476160K)] 506537K->137065K(1568448K), 3.2680080 secs]

更新:检查内核日志消息后,它是一个 oom-killer。但是系统为什么要杀掉进程,难道不是因为进程占用了大量的系统资源(内存)。

【问题讨论】:

***.com/questions/6754923/… 链接说的是本机代码,但我会尝试使用 jconsole。 您是否尝试在那些可疑的代码部分中使用“打印内存信息”?它可能会有所帮助 Java 不会使用超过指定的最大堆大小。您应该将最大堆设置为您的应用程序需要的任何值,然后确保相关服务器可以为 java 进程提供那么多内存。我在下面添加了更多详细信息。 【参考方案1】:

内存泄漏可以通过下面提到的 3 个简单步骤来解决:

第 1 步:在早期捕获堆转储

启动您的应用程序。让它占用真正的流量 10 分钟。此时捕获堆转储。堆转储基本上是您的内存快照。它包含驻留在内存中的所有对象、存储在这些对象中的值、这些对象的入站和出站引用。您可以使用以下命令捕获堆转储:

   jmap -dump:format=b,file=<file-path> <pid> 

   where

   pid: is the Java Process Id, whose heap dump should be captured
   file-path: is the file path where heap dump will be written in to.

如果您不想使用 jmap 来捕获堆转储,可以使用 capture heap dumps 的其他几个选项。

第 2 步:在应用程序崩溃之前捕获堆转储

完成第 1 步后,让应用程序运行。在应用程序崩溃之前,再次进行另一个堆转储。通常在崩溃之前捕获堆转储可能具有挑战性,因为我们不知道应用程序何时会崩溃。是在 30 分钟、3 小时、3 天之后吗?因此,最好使用以下 JVM 属性启动您的应用程序:

    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<file-path>

    file-path: is the file path where heap dump will be written in to.

此属性将在应用程序遇到 OutOfMemoryError 时触发堆转储。

第 3 步:分析堆转储

导致内存泄漏的对象在此期间不断增长。如果您可以识别在步骤#1 和步骤#2 中捕获的堆转储之间大小增加的对象,那么这些对象就是导致内存泄漏的对象。

为此,您可以考虑使用堆转储分析工具,例如 HeapHero.io 、 Eclipse MAT。当您将堆转储加载到任何工具时,将有一个部分报告内存中最大的对象。将此部分与步骤#1 和步骤#2 中捕获的堆转储进行比较。如果您注意到对象的任何异常增长,那么它们就是导致应用程序内存泄漏的对象。

【讨论】:

【参考方案2】:

我建议您查看我关于此主题的原始文章。 Java 堆转储分析一开始可能很复杂,但使用 Eclipse Memory Analyzer 等工具可以简化该过程。

JVM 堆转储对以下场景很有用:

Java 级内存泄漏的识别。 Java 级类加载器泄漏的识别。 了解有关特定负载/流量场景下 Java 应用程序内存占用的更多信息。

参考:Java heap dump: are you up to the task?

【讨论】:

【参考方案3】:

关于 java 内存泄漏的问题与 this、that 等重复。不过,这里有一些想法:

首先拍摄一些堆快照,如上面链接的答案中所述。

然后,如果您对整个应用程序了如指掌,您可以观察实例计数并找出哪个类型的实例太多。例如,如果您知道一个类是单例类,但您在内存中看到该类的 100 个实例,那么这肯定表明那里正在发生一些有趣的事情。或者,您可以比较快照以找出哪些类型的对象随着时间的推移而数量增加;这里的关键是您正在寻找在某个使用期间的相对增长。

一旦您知道泄漏了什么,您就可以追溯引用以找到无法收集的根引用。

最后,请记住,您看到 OutOfMemoryError 可能不是因为您正在泄漏内存,而是因为您的堆的某些部分对于应用程序来说太小了。要检查是否是这种情况:

在您的问题中包含您正在使用的虚拟机类型。 在您的问题中包含您在启动 java 时传递的参数。您的最小、最大和 permgen 堆大小是多少?您使用的是什么类型的垃圾收集器? 在您的问题中包含 OOME 堆栈跟踪,以防那里有一些有用的信息。 打开详细的 GC 日志记录,以便您可以看到堆的哪些部分正在增长。 打开HeapDumpOnOutOfMemoryError parameter,以便在进程结束时获得堆转储。

更新:我不确定您最新更新中的“内核因内存不足错误而终止进程”是什么意思,但我想您可能会说调用了linux out of memory killer。是这样吗?这个问题与 java OutOfMemoryError 完全不同。有关正在发生的事情的更多详细信息,请查看我刚刚链接到的页面中的链接,包括 this 和 that。但是解决您的问题的方法很简单:在有问题的服务器上使用更少的内存。我想您可以删除相关 java 进程的最小和最大堆大小,但您需要确保不会触发真正的 java OutOfMemoryErrors。你能把一些流程移到别处吗?您能否将内存杀手与特定进程的启动关联起来?

【讨论】:

我已按照您提到的步骤进行操作。我使用了 jprofiler 和 jconsole。当我从 jprofiler 运行 GC 时,它恢复了所有内存,但是当我在服务器上执行“htop”时,它仍然显示 tomcat 正在使用大量内存,并且由于内存不足异常而崩溃。我不知道为什么系统进程仍然显示正在使用的内存。 tomcat 在 centos 上运行。 理解 Java 堆转储不同于调试 OutOfMemoryError。我会用更多关于后者的信息来更新我的答案。 另外,您还没有说出在比较实例计数时发现了什么。有没有任何类型的物体数量增加?对于它的价值,我不明白“系统进程仍然显示正在使用的内存”有什么相关性,但是当OOME发生时java进程可能不会停止。 实例计数显示很多 HashMapEntry 实例。我相信这些条目来自 Hibernate,我们正在获取大量数据库记录。但最终他们都得到了 gc'ed。 Hibernate 是否缓存数据?当你比较几个堆转储时,这个缓存大小会随着时间增长吗?你怎么知道数据将从缓存中被逐出——例如是否有最大大小,缓存是否使用弱引用等?此外,如果您要加载大量数据,那么您可能将最大堆大小设置得太低。

以上是关于了解 Java 堆转储的主要内容,如果未能解决你的问题,请参考以下文章

从 Java 堆转储中获取已使用和释放的内存

内存泄漏的java堆和线程分析

了解 Apache Tomcat 6.0.26 的线程转储

解析堆转储文件

用于分析大型 Java 堆转储的工具

分析大型 Java 堆转储 - 内存错误