掘金 后端 ( ) • 2024-05-06 11:44

OutOfMemoryError是Java程序中常见的异常,通常出现在内存不足时,导致程序无法运行。

当出现OutOfMemoryError异常时,可能的现象是这样的。

  • 程序异常终止:OutOfMemoryError 通常会导致程序异常终止。JVM 无法为新对象分配内存时,会抛出该异常。
  • 堆内存不足:OutOfMemoryError 表示堆内存不足以为新对象分配空间。这可能会导致应用程序无法继续正常运行。
  • 内存泄漏:OutOfMemoryError 有时会暗示存在内存泄漏问题。即使没有明显的内存泄漏,也可能是应用程序中某些对象持续增加,导致堆空间耗尽。
  • 堆转储文件:在抛出 OutOfMemoryError 异常时,JVM 可能会生成一个堆转储文件(heap dump),记录当前堆内存的状态。可以使用该文件来分析内存使用情况和定位问题。
  • 性能下降:在出现内存不足的情况下,应用程序可能会经历性能下降,因为 JVM 可能会频繁执行垃圾回收以尝试释放内存。
  • 日志记录: 日志文件中发现 OutOfMemoryError 。异常消息通常会包含一些有关内存分配失败的信息,例如 "Java heap space"(堆空间不足)或 "GC overhead limit exceeded"(垃圾回收开销过大)。
  • 程序假死:当 JVM 的堆空间不足以分配新对象时,可能会触发垃圾回收。如果垃圾回收器尝试回收内存但无法释放足够的空间,或者由于频繁的垃圾回收导致系统资源被耗尽,程序可能会出现假死状态。表现为进程还在,但是无响应、长时间停顿。

可能的堆栈信息是这样的。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at demo.OOMDemo.main(OOMDemo.java:22)

借助MAT工具和内存泄漏产生的dump文件可以分析可能的内存泄漏代码问题定位。

什么是OutOfMemoryError异常

在 Java 中,OutOfMemoryError 是一种错误(Error),而不是异常(Exception)。

它表示 Java 虚拟机(JVM)已经耗尽了可用的内存资源,无法再分配给新的对象,导致程序无法继续执行。

OutOfMemoryError 可能由以下几种情况引起:

  • 堆内存溢出(Heap Space):当 Java 程序中创建了太多的对象,而堆内存无法满足这些对象的需求时,就会发生堆内存溢出。这通常是因为程序中存在内存泄漏(Memory Leak)或者处理大量数据时没有及时释放内存导致的。
  • 方法区溢出(PermGen Space 或 Metaspace):Java 虚拟机中的方法区用于存储类的元数据信息、静态变量、常量池等数据。当加载的类过多或者字符串常量过多时,方法区可能会溢出。在 Java 8 及之前的版本中使用的是 PermGen Space(永久代),而在 Java 8 及之后的版本中使用的是 Metaspace。溢出时会抛出相应的错误:PermGen space 或 Metaspace。
  • 栈溢出(Stack Overflow):每个线程在 Java 虚拟机中都有自己的栈空间,用于存储方法的调用栈信息。当递归调用层级过深或者方法调用过多时,栈空间可能会溢出,导致栈溢出错误。
  • 直接内存溢出:使用 NIO(New Input/Output)库进行 IO 操作时,可能会使用到直接内存(Direct Memory)。如果程序中频繁申请直接内存而没有及时释放,可能会导致直接内存溢出。

什么是dump文件

在 Java 中,Dump 文件是指在程序发生严重问题(比如崩溃或者出现内存溢出等)时,用于记录当前 JVM 运行状态的文件。Dump 文件可以包含有关 JVM 运行时的诊断信息,例如内存使用情况、线程堆栈信息、对象实例信息等,有助于开发人员分析问题并定位 bug。

通常情况下,Dump 文件主要用于以下几种情况:

  • 内存溢出(OutOfMemoryError)问题分析:当程序发生内存溢出错误时,可以生成 Dump 文件以便后续分析。Dump 文件中包含了内存堆的快照,可以查看堆中对象的分布情况,帮助开发人员找出造成内存溢出的原因。
  • JVM 崩溃问题分析:当 JVM 运行时发生崩溃,无法正常工作时,可以生成 Dump 文件以便排查问题。Dump 文件中包含了 JVM 运行时的状态信息,例如线程状态、堆栈信息等,有助于分析问题的根本原因。
  • 性能调优和分析:在进行性能调优时,Dump 文件可以提供有关 JVM 运行时的详细信息,例如线程的 CPU 占用情况、内存使用情况等,有助于分析程序的瓶颈并进行优化。

生成 Dump 文件通常需要使用 JVM 提供的工具或者命令行参数。例如,可以使用以下 JVM 参数来指定在发生 OutOfMemoryError 时生成 Dump 文件:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.log

生成的 Dump 文件通常是二进制格式的文件,可以使用专门的工具(如 Eclipse Memory Analyzer)来打开和分析。

通过分析 Dump 文件,开发人员可以更好地理解程序的运行情况,并找出问题所在。

什么是MAT工具

MAT(Memory Analyzer Tool)是一个用于 Java 应用程序内存分析的强大工具。它是一个开源项目,由 Eclipse 基金会提供支持。MAT 的主要功能是帮助开发人员分析 Java 程序的内存使用情况,特别是用于识别和解决内存泄漏问题。

MAT 工具可以帮助开发人员解决以下类型的问题:

  • 内存泄漏分析:MAT 可以通过分析 Java 堆转储(Heap Dump)文件来识别内存泄漏问题。它可以显示对象实例之间的引用关系,并帮助开发人员找出未被正确释放的对象,从而定位内存泄漏的根本原因。
  • 内存使用情况分析:MAT 可以提供详细的内存使用情况报告,包括对象实例数量、对象大小、对象引用关系等信息。这有助于开发人员了解 Java 应用程序的内存使用模式,并进行优化。
  • GC 日志分析:MAT 可以分析 JVM 输出的垃圾回收(GC)日志文件,帮助开发人员了解 GC 活动的情况,包括 GC 频率、停顿时间、对象分配速率等信息。
  • 线程分析:MAT 可以提供线程转储(Thread Dump)文件的分析,帮助开发人员识别死锁、线程阻塞等问题,并定位问题的原因。

MAT 提供了一个直观的用户界面,可以通过图形化界面进行内存分析和问题定位。它还提供了一系列的分析工具和报告,帮助开发人员深入理解 Java 应用程序的内存行为。

搜索引擎搜索 Eclipse Memory Analyzer Tool可以找到下载链接。(外链审核很严格~~)

10.png

异常发生了定位异常代码

使用 MAT 定位 OutOfMemoryError(OOM)的过程通常包括以下步骤:

收集堆转储文件:首先,需要在发生 OutOfMemoryError 异常时收集 Java 应用程序的堆转储文件。可以通过在 JVM 启动参数中添加 -XX:+HeapDumpOnOutOfMemoryError 来实现,在发生 OOM 异常时会自动生成堆转储文件。

打开 MAT 工具:打开 Memory Analyzer Tool(MAT)工具,并导入之前收集到的堆转储文件。通常,堆转储文件的格式是 .hprof

执行内存分析:在 MAT 中,可以执行各种内存分析操作,以定位导致 OutOfMemoryError 异常的原因。以下是一些常见的分析步骤:

  • 内存泄漏分析:使用 MAT 的 Leak Suspects 或 Dominator Tree 功能来查找可能导致内存泄漏的对象或对象组。这些功能会显示对象实例之间的引用关系,帮助确定哪些对象未被正确释放。
  • 对象分布分析:查看对象分布报告,了解不同类型的对象在堆中的分布情况。这有助于确定哪些类型的对象占用了大量的内存空间。
  • 最大对象分析:使用 Histogram 功能查看堆中最大的对象实例,这些对象可能是导致内存问题的主要原因。
  • 执行代码路径分析:如果堆转储文件包含了足够的信息,MAT 可以尝试生成代码路径以帮助确定哪些代码路径导致了内存问题。

定位异常代码:在进行内存分析的过程中,可以尝试定位导致 OutOfMemoryError 异常的相关代码。根据分析结果,可以查看对象的引用关系,确定哪些代码路径导致了内存泄漏或者内存消耗过大的问题。

异常没有发生定位异常代码

异常没有发生定位异常代码,需要通过jmap生成dump文件。

然后将其导入到 MAT 中进行分析。以下是生成堆转储文件的步骤:

  • 确定 Java 进程 ID:首先,需要确定正在运行的 Java 进程的进程 ID(PID)。可以使用 jps 命令查看正在运行的 Java 进程及其 PID。
  • 生成堆转储文件:使用 jmap 命令生成堆转储文件。命令格式如下:
jmap -dump:file=<文件路径> <PID>

例如,要生成名为 heapdump.hprof 的堆转储文件,可以执行以下命令:

jmap -dump:file=heapdump.hprof <PID>

这将在当前工作目录下生成一个名为 heapdump.hprof 的堆转储文件。

  • 导入堆转储文件到 MAT:将生成的堆转储文件导入到 MAT 中进行分析。打开 MAT,然后选择 File -> Open Heap Dump,然后选择生成的堆转储文件。
  • 执行内存分析:一旦堆转储文件被导入到 MAT 中,就可以执行内存分析,按照前面提到的步骤来查找内存问题。

通过这些步骤可以手动生成堆转储文件并使用 MAT 进行分析,即使没有在 OutOfMemoryError 发生时自动生成堆转储文件也可以找到问题所在。

验证demo

首先通过一段测试代码来模拟OutOfMemoryError异常。


import java.util.ArrayList;
import java.util.List;

/**
 * 用于验证oom异常
 * jvm启动参数  -Xmx200m -Xms200m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof
 *
 * @author nine
 * @since 1.0
 */
public class OOMDemo {

    public static void main(String[] args) {
        List<Object> listMock = new ArrayList<>();
        List<Object> list = new ArrayList<>();
        while (true) {
            // 此处代码用于创造oom错误
            list.add(new byte[10]);
            // 此处代码是干扰代码,因为清空了变量不会内存泄漏
            listMock.add(new byte[5]);
            listMock.clear();
        }
    }
}

启动程序运行,增加jvm参数 -Xmx200m -Xms200m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof。其中堆内存大小为200M,便于复现问题。

等待一段时间后,程序会抛出OutOfMemoryError异常。

java.lang.OutOfMemoryError: Java heap space
Dumping heap to heapdump.hprof ...
Heap dump file created [212763268 bytes in 0.572 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at demo.OOMDemo.main(OOMDemo.java:20)

同时可以在classpath下看到heapdump.hprof堆转储文件。

打开MAT,选择 File>Open Heap Dump>选择heapdump.hprof>Leak Suspects Report

MAT会分析可能的几个问题,标题是 Problem Suspect 1等

11.png

由于此处只有一段代码,分析出来的问题也就一个可能问题。

The thread java.lang.Thread @ 0xf45310d0 main keeps local variables with total size 204,667,384 (98.35%) bytes.
The memory is accumulated in one instance of “java.lang.Object[]”, loaded by “<system class loader>”, which occupies 204,666,704 (98.35%) bytes.
Significant stack frames and local variables
•demo.OOMDemo.main([Ljava/lang/String;)V (OOMDemo.java:20)◦java.util.ArrayList @ 0xf45930a8 retains 204,666,728 (98.35%) bytes


The stacktrace of this Thread is available. See stacktrace. See stacktrace with involved local variables.

Keywords
java.lang.Object[]
demo.OOMDemo.main([Ljava/lang/String;)V
OOMDemo.java:20

Details »

点击See stacktrace链接可以看到堆栈信息。

main
  at java.lang.OutOfMemoryError.<init>()V (OutOfMemoryError.java:48)
  at demo.OOMDemo.main([Ljava/lang/String;)V (OOMDemo.java:20)

这也就是发生异常的代码位置。通过修改第20行代码,将list.add(new byte[10])注释掉,可以发现oom错误消失。

注:一般堆转储文件很大,可能需要mat的启动参数来进行大文件分析。

# 打开 MemoryAnalyzer.ini 文件
# 修改启动参数为 -Xmx2048m
-startup
plugins/org.eclipse.equinox.launcher_1.6.600.v20231106-1826.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.2.800.v20231003-1442
-vmargs
--add-exports=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED
-Xmx2048m

发生OutOfMemoryError的解决办法

解决 OutOfMemoryError 异常的方法取决于具体情况和根本原因。

  • 分析堆转储文件:当发生 OutOfMemoryError 异常时,可以生成堆转储文件,通过分析该文件来定位内存泄漏或者内存使用过多的原因。
  • 优化代码:检查代码中是否存在内存泄漏或者不必要的对象持有,优化数据结构和算法以减少内存使用量。特别是要注意避免在循环中创建大量临时对象,及时释放不再需要的对象引用。
  • 检查第三方库:某些第三方库可能存在内存泄漏或者内存占用过大的问题,需要对其进行检查和优化,或者考虑更换其他库。
  • 使用更高效的数据结构和算法:选择更适合场景的数据结构和算法,以减少内存使用量和提高性能。
  • 分析内存使用情况:定期监控应用程序的内存使用情况,及时发现潜在的问题并采取相应措施。
  • 使用更轻量级的解决方案:有时可以考虑使用更轻量级的框架或工具,以减少内存消耗。

再者可以优化内存参数:

  • 增加堆内存:通过增加 JVM 的堆内存大小来提供更多的内存空间。可以通过调整 -Xmx-Xms 参数来增加堆内存的最大和初始大小。但需要注意,过大的堆内存可能会导致垃圾回收时间过长,影响程序性能。
  • 增加物理内存:如果是物理机器内存不足导致的 OutOfMemoryError,可以考虑增加物理内存来解决问题。

关于作者

来自一线全栈程序员nine的探索与实践,持续迭代中。

欢迎关注或者点个小红心~