Java基础: OOM 常见原因及解决方法

OOM 常见原因及解决方法

OOM 即 Out of Memory (内存泄漏 ),是指程序在运行时,系统无法提供足够的内存空间来满足需求,

导致程序异常终止。

在哪些情况会发生 OOM?

1 由于在程序代码中没有及时处理不再使用的对象而造成了内存泄漏。

当程序打开文件或数据库连接后没有正确关闭,会导致两种严重后果:

  1. 资源泄漏:系统资源(文件描述符、数据库连接等)被持续占用

  2. 内存泄漏:相关对象无法被垃圾回收,最终可能导致OOM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 错误示例:数据库连接未关闭
public void queryDatabase() {
    try {
        Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT * FROM large_table");
        // 处理结果集...
        // 忘记关闭 rs, stmt, conn
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

每次调用都会泄漏数据库连接,连接池被耗尽,等待获取连接的线程堆积,最终导致 OOM。

2 处理大对象或者大数组时发生 OOM

当在程序中处理大对象或者大数组时,堆空间不足会发生 OOM。

数组在内存中是一块连续的空间,创建数组时即使内存大小够用但不能满足连续的内存空间依然会发生 OOM。

1
2
3
4
5
6
class ArrayOOM {
	public static void main(String[] a) {
		// 创建大小为 10M 的 byte 数组
		byte[] largeArray = new byte[1024 * 1024 * 10];
	}
}

执行编译

javac ArrayOOM.java

指定堆空间为 8m 时

java -Xmx8m ArrayOOM

触发 OOM 异常

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at ArrayOOM.main(ArrayOOM.java:7)

3 由于硬件资源限制

当程序运行时需要创建更多对象而 JVM 没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出 OutOfMemoryError。

注意这里抛出的是 Error 而不是 Exception,因为这个问题已经严重到不足以被应用处理。

如何解决:

垂直扩展方案:

垂直扩展是通过增加单台服务器的资源配置来提高处理能力。

提升物理内存并调整 JVM 参数

# 增大堆内存
-Xms4g -Xmx4g
# 增大元空间
-XX:MaxMetaspaceSize=512m

水平扩展方案:

水平扩展是通过增加服务器数量来提高整体处理能力。

通过部署负载均衡器来分散服务器压力。

2025年6月26日 第一次编辑

2025年6月27日 第二次编辑

扩展

JVM (Java Virtual Machine) 管理的内存区域

按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:

程序计数器(Program Counter Register)

线程私有,生命周期与线程相同。

记录当前线程执行的字节码指令地址(行号指示器)

Java虚拟机栈(JVM Stack):

线程私有,随线程创建而建立。

由栈帧(Stack Frame)组成,每个方法执行时创建,包含:

  • 局部变量表(基本类型/对象引用)
  • 操作数栈(方法执行的工作区)
  • 动态链接(指向运行时常量池的方法引用)
  • 方法返回地址

本地方法栈(Native Method Stack):

线程私有,为Native方法服务。

与JVM栈区别:

服务于JNI调用的本地(C/C++)方法

由虚拟机实现决定是否合并到JVM栈(如HotSpot将两者合一)

Java堆(Java Heap):

对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。

方法区(Method Area):

线程共享,存储类型信息。

存放类元数据,运行时常量池,静态变量,JIT编译后的代码缓存。

运行时常量池(Runtime Constant Pool):

方法区的子区域。

存放类/接口/方法/字段的符号引用,字面量(如字符串常量"abc")动态生成的常量(String.intern())。

直接内存(Direct Memory):

并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。

JVM 中程序计数器是唯一不会出现 OOM 的区域。

该问题是面试官考察你对 JVM 内存模型的知识以及如何正确的管理资源以及分析解决 OOM 。

熟悉 JVM 模型可以更深入的了解 Java 程序是如何运行的以及如何优化。

附录

如何检测分析程序中的资源泄漏问题?

1 在 Linux 系统中通过 proc 命令对 Java 进程监控

找到Java进程PID

jps -lv

查看进程信息

cat /proc/<pid>/status

关键指标:

  • VmPeak: 进程使用的最大虚拟内存大小
  • VmSize: 进程当前使用的虚拟内存大小
  • VmRSS: 进程使用的物理内存大小(常驻集大小)
  • Threads: 进程包含的线程数

cat /proc/<pid>/maps

查看JVM映射的内存区域

  • Java堆内存区域
  • 线程栈区域
  • 共享库映射区域
  • JIT代码缓存区

2 使用 JVM 命令工具监控

jstat - 实时监控内存使用情况

jstat -gcutil <pid> 1000 10

参数说明:

  • -gcutil:显示垃圾收集统计信息
  • <pid>:Java进程ID
  • 1000:每1000毫秒(1秒)刷新一次
  • 10:共刷新10次

输出:

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
 0.00  25.00  78.25  45.23  95.12  90.11   15     0.250    3     0.750    1.000
  • O:老年代使用百分比(超过90%需警惕)
  • FGC:Full GC次数(突然增加可能预示OOM)

这些工具可以帮助你了解内存消耗的趋势,从而预测和避免OOM的发生。

3 分析 heapdump 堆内存信息

设置 JVM 参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof

当发生 OOM 导致程序异常退出会生成 dump 文件,可以通过 VisualVM 工具来分析内存信息。

记一次ArrayList产生的线上OOM问题

ArrayList 底层采用数组来存储数据,数组是一种线性表数据结构,它是一组连续的内存空间。

这就意味着在申请数组时如果不能满足连续的内存空间,哪怕是内存足够也会导致OOM问题。

ArrayList 当添加元素时如果当前数组大小不满足需求时就是发生扩容,默认为原来数组大小的1.5倍。

在使用ArrayList#addAll方法时,线上数据量太大,而且该方法处于循环中,就会导致扩容非常的频繁,在JVM来不及进行垃圾回收的情况下,就会导致OOM异常。

最终的解决方法:在初始化ArrayList的时候,尽量知道所需存储元素的容量或者避免其频繁扩容,就有很大的机会避免OOM异常。