每日一题--JVM内存溢出分析

JVM内存溢出(OOM)场景分析与复现指南

在 Java 开发中,内存溢出(OutOfMemoryError)是常见的 JVM 故障之一。理解其产生原因,并能够快速复现,有助于我们在实际开发中排查和预防此类问题。本文通过四个典型场景,带你深入理解 JVM 内存结构及其溢出机制。

一、JVM 内存结构回顾

在分析 OOM 之前,我们先简单回顾一下 JVM 的内存区域划分:

  • 堆(Heap):存放对象实例,也是 GC 的主要区域。
  • 方法区(Method Area):存放类信息、常量、静态变量等。在 JDK1.8 后改为元空间(MetaSpace)。
  • Java 虚拟机栈(JVM Stack):每个方法调用对应一个栈帧,存放局部变量、操作数栈、方法出口等。
  • 本地方法栈(Native Method Stack):为 JVM 调用本地方法服务。
  • 程序计数器(PC Register):记录当前线程执行的字节码行号。
  • 直接内存(Direct Memory):由 NIO 等使用,不受 JVM 堆大小限制。

二、四类 OOM 场景分析

场景1:字符串常量池溢出(stringPoolOOM

java 复制代码
public void stringPoolOOM() {
    List<String> list = new ArrayList<>();
    int i = 0;
    while (true) {
        list.add(String.valueOf(i++).intern());
    }
}
原因分析:
  • intern() 方法会将字符串放入常量池。
  • JDK1.7 后常量池位于堆中,若不断添加且不被 GC 回收,最终堆内存耗尽。
复现参数设置:
bash 复制代码
-Xms10M -Xmx10M

限制堆大小为 10MB,程序会快速抛出 java.lang.OutOfMemoryError: Java heap space


场景2:堆内存溢出(heapOOM

java 复制代码
public void heapOOM() {
    List<byte[]> list = new ArrayList<>();
    while (true) {
        list.add(new byte[1024 * 1024]);
    }
}
原因分析:
  • 不断创建大对象并持有引用,GC 无法回收。
  • 堆内存被占满后抛出 OOM。
复现参数设置:
bash 复制代码
-Xms10M -Xmx10M

与场景1相同,堆内存被迅速占满。


场景3:栈溢出(stackSOF

java 复制代码
public void stackSOF() {
    stackSOF(); // 无限递归
}
原因分析:
  • 每次方法调用都会在栈中分配一个栈帧。
  • 递归过深,栈帧超出栈容量,抛出 StackOverflowError
复现参数设置:
bash 复制代码
-Xss128k

设置每个线程的栈大小为 128KB,递归调用会更快溢出。


场景4:直接内存溢出(directMemoryOOM

java 复制代码
public void directMemoryOOM() throws Exception {
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
    while (true) {
        unsafe.allocateMemory(1024 * 1024);
    }
}
原因分析:
  • Unsafe.allocateMemory() 分配的是直接内存,不受堆大小限制。
  • 若直接内存耗尽,抛出 OutOfMemoryError
复现参数设置:
bash 复制代码
-XX:MaxDirectMemorySize=10M

限制直接内存最大为 10MB,程序会快速抛出 OOM。


三、总结与思考

内存区域 溢出类型 关键参数 触发方式
OOM -Xmx 创建大量对象并持有引用
方法区/常量池 OOM -Xmx 不断 intern 字符串
虚拟机栈 SOF -Xss 无限递归
直接内存 OOM -XX:MaxDirectMemorySize 使用 Unsafe 分配内存

补充思考

  • 堆内存:除了对象实例,还存放字符串常量池(JDK1.7+)。
  • 栈内存:每个栈帧包含局部变量表、操作数栈、动态链接、方法出口等。
  • 方法区:JDK1.8 后元空间存储类信息、常量、静态变量等,位于本地内存。
  • 直接内存:不属于 JVM 运行时数据区,但受物理内存限制。

四、写在最后

理解 JVM 内存结构是排查 OOM 的基础。通过合理设置 JVM 参数,我们可以快速复现各种内存溢出场景,从而更好地理解其触发机制。在实际开发中,建议结合监控工具(如 JVisualVM、Arthas)对内存使用进行实时分析,提前发现潜在风险。

希望这篇文章对你有所帮助,也欢迎在评论区交流你的 JVM 调优经验!


如果有新的想法,欢迎随时和我讨论。

相关推荐
专注VB编程开发20年3 小时前
vb.net,c#线程池 Dim tasks As New List(Of Task) 线程多了,后面几个可能要等一二秒后再启动
java·linux·jvm
柒.梧.3 小时前
零基础吃透Java核心基础:JDK/JRE/JVM全解析+跨平台原理
java·开发语言·jvm
weisian1519 小时前
JVM--16-面试题2:请详细描述 JVM 的运行时数据区
jvm
Drifter_yh9 小时前
「JVM」 并发编程基石:Java 内存模型(JMM)与 Synchronized 锁升级原理
java·开发语言·jvm
Coder_Boy_11 小时前
以厨房连锁故事为引,梳理Java后端全技术脉络(JVM到云原生,总结篇)
java·jvm·spring boot·分布式·spring·云原生
weisian15111 小时前
JVM--15-面试题1:谈谈你对 JVM 的理解?它的核心作用是什么?
jvm
Drifter_yh1 天前
「JVM」 深入剖析 JVM 内存结构:从底层原理到线上排查
java·jvm
何中应1 天前
使用jvisualvm提示“内存不足”
java·jvm·后端