★ JVM 内存区域如何划分?各自存放什么、哪些会抛 OOM?

结论先行:JVM 运行时内存按“是否线程共享”分为两类。线程私有区——程序计数器、虚拟机栈、本地方法栈,随线程创建和销毁;线程共享区——堆、方法区,随 JVM 启停。除程序计数器外其余区域都可能抛 OOM,而虚拟机栈深度超限会先抛 StackOverflowError。

核心区域速览

区域线程共享存放内容异常表现
程序计数器当前线程执行到的字节码行号,供分支/循环/异常/线程切换后恢复无(唯一不抛 OOM)
虚拟机栈栈帧:局部变量表、操作数栈、动态链接、返回地址深度不足抛 StackOverflowError,扩展失败抛 OOM
本地方法栈为 native 方法调用服务同虚拟机栈
几乎全部对象实例与数组,GC 主战场OOM:Java heap space
方法区类型信息、运行时常量池、静态变量、JIT 编译缓存OOM:Metaspace

版本演进要点

JDK 8 起方法区的落地实现改为元空间(Metaspace),直接使用本地内存,默认大小不设上限,避免永久代大小难以预估导致 Full GC;字符串常量池则从方法区移入堆中。对象引用(局部变量)存在栈上,对象本体在堆上。

// 堆 OOM 现场:用 -Xmx 限制堆上限,持续分配即可复现
public class HeapOomDemo {
    public static void main(String[] args) {
        java.util.List<byte[]> list = new java.util.ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]); // 每次分配 1MB 且被引用
        }
    }
}

// 栈溢出现场:无限递归撑爆单线程调用栈
public class StackOverflowDemo {
    public static void main(String[] args) {
        recurse(1);
    }
    static int recurse(int n) {
        return recurse(n + 1);
    }
}

常见追问 / 记忆点

  • 记忆口诀:三个私有(计数器、虚拟机栈、本地方法栈)+ 两个共享(堆、方法区),只有程序计数器不会 OOM。
  • 追问:JDK 8 用元空间替换永久代,顺带把字符串常量池留在堆里,减少元空间扩容引发的 Full GC。
  • 追问:栈帧里局部变量表存的是引用与基本类型,真正对象数据在堆上;-Xss 控制栈深、-Xmx 控制堆上限。
笔记加载中…