目录
6-内存泄漏故障排除教程
如果您的应用程序的执行时间变得更长,或者如果操作系统似乎执行得更慢,这可能是内存泄漏的迹象。换句话说,正在分配虚拟内存,但在不再需要时不会返回。最终应用程序或系统内存不足,应用程序异常终止。
6.1 了解 OutOfMemoryError 异常
java.lang.OutOfMemoryError在 Java 堆中分配对象的空间不足时会引发错误。
内存泄漏的一个常见迹象是java.lang.OutOfMemoryError异常。在这种情况下,垃圾收集器无法腾出空间来容纳新对象,并且无法进一步扩展堆。此外,当本机内存不足以支持 Java 类的加载时,可能会引发此错误。在极少数情况下,java.lang.OutOfMemoryError当花费过多时间进行垃圾收集并且释放的内存很少时,可能会抛出java.lang.OutOfMemoryError抛出异常时,还会打印堆栈跟踪。
当java.lang.OutOfMemoryError无法满足本机分配时(例如,如果交换空间不足),本机库代码也可能引发异常。
诊断异常的早期步骤OutOfMemoryError是确定异常的原因。是因为 Java 堆已满,还是因为本机堆已满而抛出?为了帮助您找到原因,异常的文本在末尾包含一条详细消息,如以下异常所示:
Exception in thread thread_name: java.lang.OutOfMemoryError: Java heap space
原因: Java heap space表示无法在 Java heap 中分配对象。此错误不一定意味着内存泄漏。问题可以像配置问题一样简单,其中指定的堆大小(或默认大小,如果未指定)对于应用程序来说是不够的。
在其他情况下,特别是对于长期存在的应用程序,该消息可能表明应用程序无意中持有对对象的引用,这可以防止对象被垃圾收集。这是内存泄漏的 Java 语言等价物。
笔记:应用程序调用的 API 也可能无意中持有对象引用。
此错误的另一个潜在来源是过度使用终结器的应用程序。如果一个类有一个finalize方法,那么该类型的对象在垃圾回收时不会回收它们的空间。相反,在垃圾回收之后,对象会排队等待最终确定,这将在稍后发生。在 Oracle Sun 实现中,终结器由为终结队列提供服务的守护线程执行。如果终结器线程跟不上终结队列,那么 Java 堆可能会填满,而这种类型的OutOfMemoryError会抛出异常。可能导致这种情况的一种情况是,当应用程序创建高优先级线程时,会导致终结队列以比终结器线程服务该队列的速率更快的速率增加。
如何解决: 根据实际情况调整堆大小,或者排除内存泄漏代码
Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded
原因: “GC Overhead limit exceeded”表示垃圾收集器一直在运行,Java 程序进展非常缓慢。垃圾回收后,如果 Java 进程花费大约 98% 以上的时间进行垃圾回收,并且如果它回收的堆空间少于 2%,并且在最后 5 个(编译时间常数)连续垃圾中一直这样做GC,然后通常会抛出此异常java.lang.OutOfMemoryError。因为实时数据量几乎无法放入 Java 堆中,几乎没有用于新分配的可用空间。
如何解决: 增加堆大小。超过 GC Overhead limit的java.lang.OutOfMemoryError异常可以使用命令行标志关闭。 -XX:-UseGCOverheadLimit
Exception in thread thread_name: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因: 请求的数组大小超过 VM 限制”表示应用程序(或该应用程序使用的 API)试图分配一个大于堆大小的数组。例如,如果应用程序尝试分配 512 MB 的数组,但最大堆大小为 256 MB,OutOfMemoryError则会抛出“请求的数组大小超出 VM 限制”的原因。
如何解决: 通常问题要么是配置问题(堆大小太小),要么是导致应用程序尝试创建巨大数组的错误(例如,当数组中的元素数量是使用计算尺寸不正确)。
Exception in thread thread_name: java.lang.OutOfMemoryError: Metaspace
原因: Java 类元数据(Java 类的虚拟机内部表示)分配在本机内存(这里称为元空间)中。如果类元数据的元空间已用尽,则会引发java.lang.OutOfMemoryError带有详细信息的异常。MetaSpace可用于类元数据的元空间量受参数 限制,该参数MaxMetaSpaceSize在命令行中指定。当类元数据所需的本机内存量超过MaxMetaSpaceSize时,将引发 java.lang.OutOfMemoryError带有详细信息的异常。MetaSpace
如何解决: 如果MaxMetaSpaceSize,已在命令行上设置,增加其值。MetaSpace从与 Java 堆相同的地址空间分配。减小 Java 堆的大小将为MetaSpace. 如果 Java 堆中的可用空间过多,这只是一个正确的权衡。
Exception in thread thread_name: java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space?
原因: “请求size字节为reason. 交换空间不足?”似乎是一个OutOfMemoryError异常。但是,当从本机堆分配失败并且本机堆可能接近耗尽时,Java HotSpot VM 代码会报告此明显异常。消息指示大小(以字节为单位)失败的请求和内存请求的原因。通常原因是报告分配失败的源模块的名称,尽管有时它是实际原因。
如何解决: 当抛出此错误消息时,VM 调用致命错误处理机制(即,它生成一个致命错误日志文件,其中包含有关崩溃时线程、进程和系统的有用信息)。在本机堆耗尽的情况下,日志中的堆内存和内存映射信息会很有用。请参阅致命错误日志。
如果引发此类OutOfMemoryError异常,您可能需要使用操作系统上的故障排除实用程序来进一步诊断问题。请参阅DTrace 工具。
Exception in thread thread_name: java.lang.OutOfMemoryError: Compressed class space
**原因:**在 64 位平台上,指向类元数据的指针可以用 32 位偏移量(带UseCompressedOops)表示。这由命令行标志控制UseCompressedClassPointers(默认开启)。如果UseCompressedClassPointers使用 ,则可用于类元数据的空间量固定为 amount CompressedClassSpaceSize。UseCompressedClassPointers如果超过所需的空间CompressedClassSpaceSize,则会抛出 java.lang.OutOfMemoryError带有详细压缩的类空间。
如何解决: 增加CompressedClassSpaceSize关闭UseCompressedClassPointers。注意:的可接受大小是有界限的CompressedClassSpaceSize。例如-XX: CompressedClassSpaceSize=4g,超出可接受范围将导致消息如
CompressedClassSpaceSize4294967296 无效;必须介于 1048576 和 3221225472 之间。
笔记:有不止一种类元数据、–klass元数据和其他元数据。只有klass元数据存储在以 为边界的空间中CompressedClassSpaceSize。其他元数据存储在Metaspace.
Exception in thread thread_name: java.lang.OutOfMemoryError: reason stack_trace_with_native_method
**原因:**如果错误信息的详细部分是“reason stack_trace_with_native_method,并打印堆栈跟踪,其中顶部框架是本机方法,则表明本机方法遇到分配失败。此消息与上一条消息之间的区别在于,分配失败是在 Java 本机接口 (JNI) 或本机方法中检测到的,而不是在 JVM 代码中检测到的。
如何解决: 如果引发此类OutOfMemoryError异常,您可能需要使用操作系统的本机实用程序来进一步诊断问题。请参阅本机操作系统工具。
6.2 使用 JDK Mission Control 调试内存泄漏
6.2.1 JMC检测内存泄漏
使用 JMC可以及早检测内存泄漏并防止OutOfmemoryErrors。
检测缓慢的内存泄漏可能很困难。一个典型的症状可能是由于频繁的垃圾收集,应用程序在运行很长时间后变慢了。最终,OutOfmemoryErrors可能会被看到。但是,通过分析 Java Flight 记录,可以及早检测到内存泄漏,甚至在此类问题发生之前。
观察您的应用程序的活动集是否随着时间的推移而增加。活动集是在旧收集(所有不活动的对象)之后使用并已被垃圾收集的 Java 堆的数量。要检查实时集,请打开 JMC 并使用 Java 管理控制台 (JMX) 连接到 JVM。打开MBean Browser选项卡并GarbageCollectorAggregator在com.sun.management.
打开 JMC 并开始一个时间固定记录(分析记录)一小时。在开始飞行记录之前,请确保从Memory Leak Detection设置 中选择了Object Types + Allocation Stack Traces + Path to GC Root选项。
显示了堆大小问题
录制完成后,录制文件 ( .jfr) 在 JMC 中打开。查看自动分析结果页面。要检测内存泄漏,请关注页面的Live Objects部分。这是一个记录的示例图,它显示了堆大小问题:
在Heap Live Set Trend部分观察到,堆上的 live set 似乎在迅速增加,并且对参考树的分析检测到了一个泄漏候选者。
显示了内存泄漏问题
如需进一步分析,请打开Java Applications页面,然后单击Memory页面。这是一个记录的示例图,它显示了内存泄漏问题。
从图中可以观察到内存使用量稳步增加,这表明存在内存泄漏问题。
您可以观察到大多数被跟踪的活动对象实际上是由 持有的Leak$DemoThread,而后者又持有一个泄露的char[]类。如需进一步分析,请参阅“结果”选项卡中包含幸存对象采样的Old Object Sample事件。此事件包含分配时间、分配堆栈跟踪、返回 GC 根的路径。
您可以使用 Java Flight Recordings 来识别泄漏类。
查找泄漏类型
要查找泄漏类型,请打开Memory页面并单击Live Objects页面。这是一个记录的示例图,它显示了泄漏类型。
可以观察到大多数被跟踪的活动对象实际上是由 持有的Leak$DemoThread,而后者又持有一个泄露的char[]类。如需进一步分析,请参阅“结果”选项卡中包含幸存对象采样的Old Object Sample事件。此事件包含分配时间、分配堆栈跟踪、返回 GC 根的路径.
对象分配位置显示 TLAB 分配
当识别出潜在的泄漏类时,请查看JVM Internals页面中的TLAB Allocations页面以获取一些对象分配位置的示例。这是一个记录的示例图,它显示了 TLAB 分配。
6.2.2 jfr 工具
Java Flight Recorder (JFR) 记录有关 Java 运行时和在 Java 运行时上运行的 Java 应用程序的详细信息。此信息可用于识别内存泄漏。
要检测内存泄漏,JFR 必须在泄漏发生时运行。JFR 的开销非常低,不到 1%,并且它被设计为可以安全地在生产中始终运行。
在应用程序启动时使用java如下示例所示的命令开始录制:
java -XX:StartFlightRecording
当 JVM 内存不足并因java.lang.OutOfMemoryError错误而退出时,带有前缀的记录hs_oom_pid通常(但不总是)写入启动 JVM 的目录。获取记录的另一种方法是使用该工具在应用程序内存不足之前将其转储jcmd,如以下示例所示:
jcmd pid JFR.dump filename=recording.jfr path-to-gc-roots=true
录制后,使用目录中的jfr工具java-home/bin打印包含潜在内存泄漏信息的旧对象样本事件。以下示例显示命令和 pid 为 16276 的应用程序记录的输出示例:
jfr print --events OldObjectSample pid16276.jfr
...
jdk.OldObjectSample {
startTime = 18:32:52.192
duration = 5.317 s
allocationTime = 18:31:38.213
objectAge = 74.0 s
lastKnownHeapUsage = 63.9 MB
object = [
java.util.HashMap$Node
[15052855] : java.util.HashMap$Node[33554432]
table : java.util.HashMap Size: 15000000
map : java.util.HashSet
users : java.lang.Class Class Name: Application
]
arrayElements = N/A
root = {
description = "Thread Name: main"
system = "Threads"
type = "Stack Variable"
}
eventThread = "main" (javaThreadId = 1)
}
...
jdk.OldObjectSample {
startTime = 18:32:52.192
duration = 5.317 s
allocationTime = 18:31:38.266
objectAge = 74.0 s
lastKnownHeapUsage = 84.4 MB
object = [
java.util.HashMap$Node
[8776975] : java.util.HashMap$Node[33554432]
table : java.util.HashMap Size: 15000000
map : java.util.HashSet
users : java.lang.Class Class Name: Application
]
arrayElements = N/A
root = {
description = "Thread Name: main"
system = "Threads"
type = "Stack Variable"
}
eventThread = "main" (javaThreadId = 1)
}
...
jdk.OldObjectSample {
startTime = 18:32:52.192
duration = 5.317 s
allocationTime = 18:31:38.540
objectAge = 73.7 s
lastKnownHeapUsage = 121.7 MB
object = [
java.util.HashMap$Node
[393162] : java.util.HashMap$Node[33554432]
table : java.util.HashMap Size: 15000000
map : java.util.HashSet
users : java.lang.Class Class Name: Application
]
arrayElements = N/A
root = {
description = "Thread Name: main"
system = "Threads"
type = "Stack Variable"
}
eventThread = "main" (javaThreadId = 1)
}
...
要识别可能的内存泄漏,请查看记录中的以下元素
如果应用程序使用该-XX:StartFlightRecording:settings=profile选项启动,则记录还包含分配对象的堆栈跟踪,如以下示例所示:
stackTrace = [
java.util.HashMap.newNode(int, Object, Object, HashMap$Node) line: 1885
java.util.HashMap.putVal(int, Object, Object, boolean, boolean) line: 631
java.util.HashMap.put(Object, Object) line: 612
java.util.HashSet.add(Object) line: 220
Application.storeUser(String, String) line: 53
Application.validate(String, String) line: 48
Application.login(String, String) line: 44
Application.main(String[]) line: 30
]
查看原文,技术咨询与支持,可以扫描微信公众号进行回复咨询