如何调试Java中的OutOfMemory异常?

20
什么是调试java.lang.OutOfMemoryError异常的最佳方法?
当这种情况发生在我们的应用程序中时,我们的应用服务器(Weblogic)会生成一个堆转储文件。我们应该使用堆转储文件吗?我们应该生成Java线程转储吗?这两者到底有什么区别?
更新:生成线程转储的最佳方法是什么?在Solaris上,使用kill -3(我们的应用程序运行在Solaris上)杀死应用程序并生成线程转储是最好的方法吗?是否有一种方法可以生成线程转储但不杀死应用程序?

1
你的意思是它会生成一个.hprof文件,还是其他什么文件? - skaffman
一个phd格式的堆转储文件 - Marcus Leon
8个回答

13
在Java中分析和修复内存溢出错误非常简单。
在Java中,占用内存的对象都与其他对象链接在一起,形成一个巨大的树。想法是找到树的最大分支,这通常会指向内存泄漏情况(在Java中,当您忘记删除对象时,不会泄漏内存,而是当您忘记忘记该对象,即在某个地方保留对它的引用时)。
第1步。在运行时启用堆转储
使用-XX:+ HeapDumpOnOutOfMemoryError -XX:HeapDumpPath = / tmp 运行进程。
(始终启用这些选项是安全的。根据需要调整路径,它必须可由java用户写入)
第2步。重现错误
让应用程序运行,直到发生 OutOfMemoryError
JVM将自动编写类似 java_pid12345.hprof 的文件。
第3步。获取转储
java_pid12345.hprof 复制到计算机上(它至少与最大堆大小一样大,因此可能会很大-如果需要,请对其进行gzip)。
第4步。使用IBM的Heap Analyzer或Eclipse的Memory Analyzer打开转储文件

堆分析器将向您展示所有在错误发生时存活的对象的树形结构。很可能它会在打开时直接指出问题所在。

IBM HeapAnalyzer

注意:给HeapAnalyzer足够的内存,因为它需要加载整个转储文件!
java -Xmx10g -jar ha456.jar

第5步. 确定堆使用最多的区域

浏览对象树,识别不必要的对象。

请注意,也可能发生所有对象都是必需的情况,这意味着您需要更大的堆。适当地调整堆的大小和配置

第6步. 修复您的代码

确保只保留您实际需要的对象。及时从集合中删除项目。确保不保留对不再需要的对象的引用,只有这样它们才能被垃圾回收。


7
我曾成功使用Eclipse Memory Analyzer (MAT)Java Visual VM的组合来分析堆转储。MAT有一些报告可以运行,给你一个大致的想法,让你关注代码中哪些方面需要努力。在我看来,VisualVM有更好的界面,可以实际检查你感兴趣的各种对象的内容。它有一个过滤器,可以显示特定类的所有实例,并查看它们的引用和自引用。我已经有一段时间没有使用这两个工具进行分析了,现在它们可能有更接近的功能集。在当时,同时使用这两个工具对我很有帮助。

这些也是我使用的工具。我更喜欢MAT,因为它需要比VisualVM更少的内存来加载大型堆转储文件,尽管我必须承认VisualVM界面更易于使用。 - Guillaume

6

调试 java.lang.OutOfMemoryError 异常的最佳方法是什么?

OutOfMemoryError 描述了错误的类型。您需要检查错误消息的描述以处理异常。

内存不足异常有各种根本原因。请参考 Oracle 文档页面 获取更多详细信息。

java.lang.OutOfMemoryError: Java heap space:

原因:Java 堆空间的详细消息表示无法在 Java 堆中分配对象。

java.lang.OutOfMemoryError: GC Overhead limit exceeded:

原因:"GC overhead limit exceeded" 详细消息表示垃圾回收器一直运行,而 Java 程序进展非常缓慢。

java.lang.OutOfMemoryError: Requested array size exceeds VM limit:

原因:详细信息"请求的数组大小超过了VM限制"表示应用程序(或该应用程序使用的API)尝试分配一个比堆大小更大的数组。
java.lang.OutOfMemoryError: Metaspace:
原因:Java类元数据(Java类的虚拟机内部表示)在本地内存中分配(这里称为metaspace)。
java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space?:
原因:"request size bytes for reason. Out of swap space?"的详细信息似乎是一个OutOfMemoryError异常。然而,当从本地堆中分配失败并且本地堆可能接近耗尽时,Java HotSpot VM代码会报告此明显的异常。
java.lang.OutOfMemoryError: Compressed class space:
原因:在64位平台上,类元数据的指针可以由32位偏移量表示(使用UseCompressedOops)。这由命令行标志UseCompressedClassPointers控制(默认情况下打开)。
如果使用了UseCompressedClassPointers,类元数据可用空间的大小将固定为CompressedClassSpaceSize。如果UseCompressedClassPointers所需的空间超过了CompressedClassSpaceSize,将抛出一个带有详细信息"Compressed class space"的java.lang.OutOfMemoryError我们应该使用堆转储文件吗?我们应该生成Java线程转储吗?两者之间到底有什么区别? 是的。您可以使用这个堆转储文件来使用像visualvmmat这样的性能分析工具来调试问题。您可以使用线程转储来进一步了解线程的状态。 生成线程转储的最佳方法是什么?在Solaris上运行的应用程序中,kill -3是杀死应用程序并生成线程转储的最佳方法吗?有没有一种方法可以生成线程转储而不杀死应用程序? kill -3 <process_id>会生成线程转储,而且这个命令不会杀死Java进程。

3
您也可以使用jmap/jhat附加到正在运行的Java进程。如果您需要调试正在运行的应用程序,则这些工具非常有用。
您还可以将jmap保留为cron任务,记录到文件中,稍后可以进行分析(我们发现这对于调试实时内存泄漏非常有用)。
jmap -histo:live <pid> | head -n <top N things to look for> > <output.log>

Jmap还可以使用-dump选项生成堆转储,可以通过jhat读取。
请参见以下链接以获取更多详细信息 http://www.lshift.net/blog/2006/03/08/java-memory-profiling-with-jmap-and-jhat 这是另一个要加书签的链接 http://java.sun.com/developer/technicalArticles/J2SE/monitoring/

jstack应该打印出Java进程的堆栈跟踪。http://download.oracle.com/javase/1.5.0/docs/tooldocs/share/jstack.html - Nauman

3
通常情况下,调试OutOfMemoryError问题非常困难。我建议使用性能分析工具。JProfiler效果不错,我以前用过,它非常有帮助,但我相信还有其他同样好的工具。
回答您的具体问题:
堆转储是整个堆的完整视图,即使用new创建的所有对象。如果内存不足,则这将非常大。它显示了每种类型的对象数量。
线程转储向您显示每个线程的堆栈,显示转储时每个线程在代码中的位置。请记住,任何线程都可能导致JVM耗尽内存,但实际上抛出错误的可能是另一个线程。例如,线程1分配填满所有可用堆空间的字节数组,然后线程2尝试分配1字节数组并抛出错误。

2

1

一旦您获得了查看堆转储的工具,请查看线程堆栈中处于运行状态的任何线程。它可能是那些出现错误的线程之一。有时,堆转储将告诉您哪个线程在顶部出现了错误。

这应该指向正确的方向。然后使用标准调试技术(日志记录、调试器等)来锁定问题。使用Runtime类获取当前内存使用情况,并在涉及的方法或进程执行时记录它。


1
通常情况下,找到导致OutOfMemoryError的线程并没有什么帮助。当然,它可能是压垮骆驼的最后一根稻草,但这最后一根稻草总是有罪吗?此外,“标准调试技术”在您无法可靠地重现问题时也不会有太大帮助。 - meriton
根据我的经验(自夸一下,我相当有经验),OutOfMemoryError通常发生在导致问题的方法中。当然你是正确的,这并非总是如此。但由于Java不会真正遭受内存泄漏的困扰,这些错误通常是由于某些重型处理(文本或XML)在单个线程中执行,并且将是获得错误的同一个线程。但你是对的,其他“无辜”的线程也可能出现错误。 - Fraggle

1
我通常使用Eclipse Memory Analyzer。它显示了疑似罪犯(占用堆转储最多的对象)和生成这些对象的不同调用层次结构。一旦有了这个映射,我们就可以回到代码并尝试理解代码路径中是否存在任何可能的内存泄漏。
然而,OOM并不总是意味着存在内存泄漏。应用程序在稳定状态或负载下需要的内存可能在硬件/VM中不可用。例如,可能存在一个32位Java进程(最大内存使用~4GB),而VM仅具有3GB。在这种情况下,最初应用程序可能运行良好,但随着内存需求接近3GB,可能会遇到OOM。
正如其他人所提到的,捕获线程转储不费什么代价,但捕获堆转储则很费。我观察到,在捕获堆转储时,应用程序(通常)会冻结,只有通过终止然后重新启动才能恢复。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接