Java SE 6 故障排除指南 – 3、内存泄露
内存泄露故障排除
如果你的应用程序执行的时间越来越长,或如果操作系统执行越来越慢,这可能是内存泄露的指示。换句话说,虚拟内存被分配但在不需要时没有归还。最终应用程序或系统没有可用内存,应用程序非正常终止。
这篇文章提供了一些涉及内存泄露的问题诊断的建议。
3.1 OutOfMemoryError 的含义
一个最常见的内存泄露的指示是 java.lang.OutOfMemoryError 错误。这个错误在Java堆或堆的特定区域没有足够空间用于分配对象时抛出。垃圾收集器不能创造更多可用空间来容纳一个新的对象,堆也不能扩展。
当 java.lang.OutOfMemoryError 错误抛出时,栈轨迹也会被打印。
java.lang.OutOfMemoryError 也可以被本地库代码抛出,当本地分配不能满足时,例如,交换空间很低。
诊断 java.lang.OutOfMemoryError 的一个早期步骤是确定错误的含义。它是否意味着Java堆满了,或意味着本地堆满了?为帮助你回答这个问题,下面的子章节解释了一些可能的错误信息,还有连接到更详细信息的链接:
- “main” 线程里的 java.lang.OutOfMemoryError 异常:Java 堆空间。详见 3.1.1。
- “main” 线程里的 java.lang.OutOfMemoryError 异常:Java PermGen空间。详见 3.1.2。
- “main” 线程里的 java.lang.OutOfMemoryError 异常:请求的数组大小超过VM限制。详见 3.1.3。
- “main” 线程里的 java.lang.OutOfMemoryError 异常:
request <size> bytes for <reason>. Out of swap space?
- request bytes for . Out of swap space? 详见 3.1.4
- “main” 线程里的 java.lang.OutOfMemoryError 异常:
java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
,详见 3.1.5
3.1.1 Java堆空间详细信息
Java堆空间详细信息指出了对象不能再Java里分配。这个错误不是必然表示内存泄露。这个问题可能仅仅只是配置问题,指定的堆大小不能满足应用程序。
在其他情况下,特别是长期存活的应用程序,这个消息可能指示了应用程序无意地持有对象的引用,这导致对象不能被垃圾收集。这是Java语言等价的内存泄露。注意,应用程序调用的API也可能无意地持有对象的引用。
另一个潜在的可能抛出 OutOfMemoryError 的来源是应用程序过多使用finalizer。如果类有 finalize 方法,那么那种类型的对象在垃圾收集时不回收它们的空间。而是,在垃圾收集后,对象排队等待 finalization,这在稍后的时间发生。在 Sun 的实现里,finalizer由一个守护线程执行,服务于 finalization 队列。如果 finalizer 线程不能跟上 finalization 队列,那么Java堆将充满, OutOfMemoryError 将被抛出。导致折衷情况的一个情景是应用程序创建高优先级线程,导致finalization 队列增加的速率快于 finalizer 线程服务那个队列的速率。3.3.6节讨论了如何管理等待finalization的对象。
3.1.2 PermGen 空间详细信息
PermGen 空间详细信息指示了永久代是满的。永久代是堆的一块区域,存储了类和方法对象(元数据)。如果应用程序加载了大量的类,那么永久代的大小可能需要增加,用 -XX:MaxPermSize
选项。
interned的 java.lang.String 对象也存储在永久代。java.lang.String 类维持了一个字符串池。当 intern
方法调用时,这个方法检查池来看等价的字符串是否已存在于池里。更准确的角度, java.lang.String.intern
方法用于获取字符串的标准表示,结果是同一类的实例的引用将被返回,如果字符串作为标量出现。如果应用程序intern了大量的字符串,永久代可能需要增长。
当这类错误发生时,文本 String.intern
或 ClassLoader.defineClass
可能出现在栈轨迹的附近。
jmap -permgen
命令打印永久代里对象的统计信息,包括 internalized 的字符串实例信息。见 2.7.4 获取永久代信息。
3.1.3 请求的数组大小超过VM限制 详细信息
“请求的数组大小超过VM限制” 详细信息指示了应用程序尝试分配一个数组,它的大小大于堆的大小。例如,应用程序尝试分配 512MB 的数组,但堆大小的最大值是 256MB,那么 OutOfMemoryError 将以 “请求的数组大小超过VM限制” 的理由抛出。大多数情况下,这个问题要么是配置问题,要么是应用程序的bug,尝试创建一个超级大的数组。
3.1.4 request <size> bytes for <reason>. Out of swap space?
详细信息
这个信息以 OutOfMemoryError 形式出现。然而,HotSpot VM代码报告这个表面异常,当从本地堆分配失败和本地堆可能接近耗尽。这个消息指示了失败的请求的大小(按字节),和内存请求的理由。大多数情况下消息的 <reason>
部分是报告分配失败的源模块的名字,虽然有些情况下它指示了理由。
当这个错误消息被抛出时,VM发起致命错误处理机制,它生成一个致命错误日志文件,包含了关于线程、进程和崩溃时系统的有用信息。在本地堆耗尽的情况下,日志里的堆内存和内存映射信息将有用。
如果这类 OutOfMemoryError 被抛出,你可能需要使用操作系统上的故障排除实用程序来诊断议题issue。
这个问题可能跟应用程序无关,例如:
- 操作系统配置了不充足的交换空间。
- 系统上的另一个进程消耗了所有内存资源。
3.1.5 java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
详细信息
如果在错误信息的详细部分是 <reason> <stack trace> (Native method)
,且栈轨迹将被打印,最顶部的帧是本地方法,那么这指示了本地方法遭遇了分配失败。这个与前一个消息的区别是分配失败是在JNI或本地方法而不是在Java VM 代码里检测到。
3.2 崩溃而不是 OutOfMemoryError
有时候应用程序在本地堆分配失败后马上就崩溃了。这在本地代码不检查内存分配函数返回的错误时出现。
例如,如果没有可用内存时 malloc
系统调用返回 NULL。如果 malloc
的返回没有被检查,当尝试访问一个非法内存地址时应用程序可能崩溃。依赖于环境,这种类型的issue可能很难定位。
然而,来自致命错误日志或崩溃转储的信息足以诊断这个issue。致命错误日志在 附录C 里。如果崩溃的原因被确定为没有检查分配失败,那么分配失败的理由必须被验证。与其他本地堆issue一样,系统可能没有配置足够的交换空间,系统里的其它进程消耗了所有内存资源,或应用程序里有泄露,导致系统没有内存。
3.3 Java 语言代码里的泄露诊断。
诊断Java语言代码里的泄露是个困难的任务。大多数情况下,它要求对应用程序非常详细的知识。另外过程通常是反复和冗长的。这节提供了下面的子章节:
- 3.3.1 NetBeans Profiler
- 3.3.2 使用 jhat 实用程序
- 3.3.3 创建堆转储
- 3.3.4 获取正在运行进程的堆 histogram
- 3.3.5 在 OutOfMemoryError 时获取堆的histogram
- 3.3.6 监视等待 finalization 对象的数量
- 3.3.7 第三方内存调试器
3.3.1 NetBeans Profiler
NetBeans Profiler 是个非常出色的剖析器,可以快速定位内存泄露。多数商业内存泄露调试工具需要很长时间来定位大应用程序里的泄露。NetBeans Profiler 使用内存分配模式和回收这些对象的典型通常证明。这个处理液包括缺乏内存回收。剖析器能够检查这些对象在哪里分配,在大多情况下这足以识别导致泄露的根源。
更多信息见 http://profiler.netbeans.org。
3.3.2 使用 jhat 实用程序
jhat 工具队调试无意保留的对象(或内存泄露)有用。它提供了一种方法来浏览对象转储,查看堆里的所有可达对象,知道那个引用保留了存活对象。
为使用 jhat 你必须获取一个或多个正在运行程序的堆转储,这些转储必须是二进制格式的。一旦转储文件创建,它可以作为 jhat 的输入,见 2.5 节。
3.3.3 创建堆转储
堆转储提供了堆内存分配的详细信息。
3.3.3.1 HPROF 剖析器
HPROF 剖析器代理可以创建在执行程序的堆转储。下面是命令行示例:
java -agentlib:hprof=file=snapshot.hprof,format=b application
。
如果VM是嵌入式的或不是用命令行启动器启动的,那么可以用 JAVA_TOOLS_OPTIONS
环境变量来自动把 -agentlib
选项加入命令行,见 A.2 。
一旦程序与HPROF一起运行,堆转储可以在程序命令行用 Ctrl-\
或 Ctrl-Break
创建。在 Solaris OS 和 Linux上一个可选的方法是发送 QUIT
信号,用 kill -QUIT pid
命令。当信号被接受后,堆转储将被创建;在上面的例子里,文件 snapshot.hprof
将被创建。
堆转储文件保护所有原始数据和栈轨迹。
一个转储文件可以包含多个堆转储。如果 Ctrl-\
或 Ctrl-Break
被按了多次,后续的转储将被追加到文件。jhat 工具用 # n
语法来区别转储, n
是转储号。
3.3.3.2 jmap 工具
也可以用 jmap 工具来获取堆转储: jmap -dump:format=b,file=snapshot.jmap process-pid
。
不管JVM如何启动,jmap 工具可以生成堆转储快照,在上面的例子里,文件被叫作 snapshot.jmap
。jmap 输出文件应该包含所有原始数据,但不包括任何显示对象在哪里创建的栈轨迹。
3.3.3.3 JConsole 工具
获取堆转储的另一个方法是用JConsole工具,选择 HotSpotDiagnostic
MBean,显示 Operations,选择 dumpHeap 选项。
3.3.3.4 -XX:+HeapDumpOnOutOfMemoryError
命令行选项
如果指定了 -XX:+HeapDumpOnOutOfMemoryError
命令行选项,且如果抛出了 OutOfMemoryError
,VM将生成堆转储。
3.3.4 在运行进程上获取堆 histogram
你可以通过检查堆histogram来尝试快速缩小内存泄露范围。可以用下面的方法获取这个信息:
- 用命令
jmap -histo pid
来获取。输出显示了总的大小和堆里每种类型的实例数量。如果一序列histogram被获取(例如每隔2分钟),你将可以观察到一个趋势,引向进一步分析。 - 在Solaris OS和Linux上,jmap工具也可以从core文件提供histogram。
- 如果Java进程是带
-XX:+PrintClassHistogram
选项启动,Ctrl-Break 处理器将生成堆histogram。
3.3.5 OutOfMemoryError 时获取堆histogram
如果指定了 -XX:+HeapDumpOnOutOfMemoryError
选项,且如果抛出了 OutOfMemoryError
,VM将生成堆转储。你可以用 jmap 工具从堆转储获取histogram。
如果抛出 OutOfMemoryError 时生成了core文件,你可以在core文件上执行 jmap 来获得 histogram,如下:
$jmap -histo \ /java/re/javase/6/latest/binaries/solaris-sparc/bin/java core.27421
Attaching to core core.27421 from executable
/java/re/javase/6/latest/binaries/solaris-sparc/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 1.6.0-beta-b63
Iterating over heap. This may take a while...
Heap traversal took 8.902 seconds.
Object Histogram:
Size Count Class description
-------------------------------------------------------
86683872 3611828 java.lang.String
20979136 204 java.lang.Object[]
403728 4225 * ConstMethodKlass
306608 4225 * MethodKlass
220032 6094 * SymbolKlass
152960 294 * ConstantPoolKlass
108512 277 * ConstantPoolCacheKlass
104928 294 * InstanceKlassKlass
68024 362 byte[]
65600 559 char[]
31592 359 java.lang.Class
27176 462 java.lang.Object[]
25384 423 short[]
17192 307 int[]
:
示例表明OutOfMemoryError 是由 java.lang.String 对象的数量(堆里有3611828个实例)导致的。没有进一步分析是不知道字符串是在哪分配的。然而信息仍然是有用的,继续调查可以用 HPROF 或 jhat 来找出字符串是在哪分配的,和哪些引用让它们保持存活,阻止它们被垃圾回收。
3.3.6 监视等待 finalization 的对象数量
如3.1.1节提到,过度使用 finalizer可以导致 OutOfMemoryError 。你有多种选择来监视等待finalization 的对象数量:
- JConsole 管理工具可用于监视等待finalization的对象数量。这个工具报告等待finalization的计数,在内存统计信息的 “Summary” 标签面板。这个计数是估算的,但它可用于描绘应用特征,明白它依赖于大量finalization。
- 在Solaris OS和Linux上,
jmap -finalization
选项打印等待finalization的对象信息。 - 应用可以用
java.lang.management.MemoryMXBean
类的getObjectPendingFinalizationCount
方法来报告大约的等待finalization对象的数量。
3.3.7 第三方内存调试器
除了前面章节提到的工具,还有很多第三方的内存调试器可用。JProbe from Quest Software, and OptimizeIt from Borland are two examples of commercial tools with memory debugging capability. There are many others and no specific product is recommended。
3.4 诊断本地代码里的泄露
有些技术可用于找出和隔离本地代码内存泄露。一般没有单一理想的解决方案适用于所有平台。
3.4.1 跟踪所有内存分配和释放调用
一个最常见的实践是跟踪所有的本地分配和释放。这可能是个非常简单的过程也可以是非常复杂的。
3.4.2 跟踪JNI库里的内存分配
TODO
3.4.3 用OS支持跟踪内存分配
TODO
3.4.4 用 dbx 查找泄露
TODO
3.4.5 用 libumem 查找泄露
TODO