JVM 运行时内存区域如何划分的?

#Java#面试八股

JVM 运行时内存区域如何划分的?

结论先行:按线程归属划分最清晰:线程私有有程序计数器、虚拟机栈、本地方法栈,随线程生死;线程共享有堆、方法区(JDK 8+ 落地为元空间)。此外还有规范之外但真实存在的直接内存(direct memory)与代码缓存(CodeCache)。除程序计数器外,每个区域都有对应的 OOM 消息,答题时把「区域 → 存什么 → 对应参数 → 异常消息」串成一条线最稳。

一、区域总表

区域线程共享存放关键参数异常
程序计数器否当前字节码行号无无
虚拟机栈否栈帧:局部变量表、操作数栈、动态链接、返回地址-XssStackOverflowError / OOM
本地方法栈否native 方法调用帧-Xss同上
堆是对象实例、数组、字符串池(JDK 7+)-Xms、-XmxJava heap space
方法区 / 元空间是类元数据、运行时常量池、静态变量-XX:MetaspaceSize、-XX:MaxMetaspaceSizeMetaspace
直接内存是ByteBuffer.allocateDirect、Netty ByteBuf-XX:MaxDirectMemorySizeDirect buffer memory
代码缓存是JIT 编译后的机器码-XX:ReservedCodeCacheSize编译线程停止,性能下降

二、栈帧结构

虚拟机栈(每个线程一份)
├── 栈帧(每次方法调用压入一个)
│   ├── 局部变量表:this、参数、局部变量(槽位 4 字节,long/double 占 2 槽)
│   ├── 操作数栈:字节码运算的临时工作区
│   ├── 动态链接:指向运行时常量池中该方法的方法引用
│   └── 返回地址:方法正常/异常退出后回到哪
└── 栈深度上限由 -Xss 决定

三、堆的分代与分配

堆(-Xmx)
├── 新生代(Young)
│   ├── Eden(-XX:SurvivorRatio 决定占比)
│   ├── Survivor S0
│   └── Survivor S1
└── 老年代(Old)

对象分配优先走 TLAB(线程本地分配缓冲),避免多线程争用 Eden 指针
public class RegionDemo {
    public static void main(String[] args) {
        // 堆:对象实例
        byte[] onHeap = new byte[8 * 1024 * 1024];

        // 直接内存:不受 -Xmx 约束,受 -XX:MaxDirectMemorySize 与物理内存约束
        java.nio.ByteBuffer direct = java.nio.ByteBuffer.allocateDirect(8 * 1024 * 1024);

        System.out.println("heap=" + onHeap.length + " direct=" + direct.capacity());
    }
}
# 分别把两个区域打爆,观察不同的 OOM 消息
java -Xmx32m -cp . RegionDemo                      # OutOfMemoryError: Java heap space
java -Xmx512m -XX:MaxDirectMemorySize=16m -cp . RegionDemo   # OutOfMemoryError: Direct buffer memory
java -Xss256k -cp . RecursionDemo                  # StackOverflowError

四、规范与实现的差异

概念JVM 规范HotSpot 实现
方法区逻辑区域,规范未规定实现方式JDK 7 永久代 → JDK 8 元空间(本地内存)
运行时常量池属于方法区元空间内,字符串字面量落堆
字符串常量池未单独定义JDK 7 起从永久代移入堆
栈未规定是否合并虚拟机栈与本地方法栈合并实现

这就是为什么「方法区 OOM」在 JDK 8 之后变成「Metaspace OOM」——永久代被移除,元空间改用本地内存。

五、常见追问

追问回答要点
为什么程序计数器不会 OOM它只存一个行号偏移,大小固定,且是唯一没有规定 OOM 的区域
栈溢出和堆溢出怎么区分栈溢出是 StackOverflowError(Error 子类,非 OOM),堆溢出是 OutOfMemoryError: Java heap space
直接内存算 JVM 内存吗不算堆,但算进进程 RSS;容器里最容易被忽略,是 OOMKilled 的常见原因
元空间为什么不设上限默认无上限,类多时按需增长;生产建议设 MaxMetaspaceSize 兜底
对象一定分配在堆上吗不一定,逃逸分析后可能栈上分配或标量替换,但这是 JIT 优化,不是语言语义

小结

七个区域、两条线程归属线、四类异常消息,构成内存划分的完整答案。把「直接内存」和「代码缓存」补上,答案就从课本级别升到能解释线上问题的级别。

笔记加载中…