面试必问的 JVM 内存模型,一张图画清楚
一、引言
"请谈谈 JVM 的内存模型?"
这是 Java 面试中最常见的问题之一,也是考察候选人基础是否扎实的关键问题。很多人能说出堆、栈、方法区这些名词,但要讲清楚每个区域的作用、特点和 OOM 场景,就需要对 JVM 内存模型有深入的理解。
今天,我用一张全景图带你彻底搞懂 JVM 内存模型。
二、JVM 内存模型全景图
┌──────────────────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区域 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 线程私有区域 │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 程序计数器 (Program Counter) │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 作用: 记录当前线程执行的字节码行号 │ │ │ │
│ │ │ │ 特点: 唯一不会 OOM 的区域, 线程私有 │ │ │ │
│ │ │ │ 存储: 当前执行指令地址、native 方法返回值 │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 虚拟机栈 (Java Virtual Machine Stack) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 栈帧 (Stack Frame) │ │ │ │
│ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ 局部变量表 (Local Variable Table) │ │ │ │ │
│ │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ this, 参数, 局部变量, 64位类型占2槽位 │ │ │ │ │ │
│ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ ├───────────────────────────────────────────────────┤ │ │ │ │
│ │ │ │ │ 操作数栈 (Operand Stack) │ │ │ │ │
│ │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ 临时操作数存储, 方法调用参数传递 │ │ │ │ │ │
│ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ ├───────────────────────────────────────────────────┤ │ │ │ │
│ │ │ │ │ 动态链接 (Dynamic Linking) │ │ │ │ │
│ │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ 指向运行时常量池中方法的符号引用 │ │ │ │ │ │
│ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ ├───────────────────────────────────────────────────┤ │ │ │ │
│ │ │ │ │ 方法返回地址 (Return Address) │ │ │ │ │
│ │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ 方法调用前的PC值, 用于异常或正常返回 │ │ │ │ │ │
│ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 特点: 线程私有, LIFO, 栈深度固定 │ │ │
│ │ │ OOM: StackOverflowError(递归太深), OutOfMemoryError(栈太多) │ │ │
│ │ │ 参数: -Xss (栈大小, 默认1MB) │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 本地方法栈 (Native Method Stack) │ │ │
│ │ │ │ │ │
│ │ │ 作用: 为 native 方法(如JNI)提供栈空间 │ │ │
│ │ │ 特点: 与虚拟机栈类似, 但服务于 native 方法 │ │ │
│ │ │ OOM: StackOverflowError, OutOfMemoryError │ │ │
│ │ │ 参数: -Xoss (本地方法栈大小) │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 线程共享区域 │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 堆 (Heap) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 新生代 (Young Generation) │ │ │ │
│ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Eden Space │ │ │ │ │
│ │ │ │ │ 作用: 新对象分配区 │ │ │ │ │
│ │ │ │ │ 容量: 约占新生代的80% │ │ │ │ │
│ │ │ │ │ GC: Minor GC 时存活对象进入 Survivor │ │ │ │ │
│ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Survivor Space 0 (S0/From) │ │ │ │ │
│ │ │ │ │ 作用: 存放 Minor GC 后存活的对象 │ │ │ │ │
│ │ │ │ │ 容量: 约占新生代的10% │ │ │ │ │
│ │ │ │ │ GC: 与 S1 互相复制, 年龄达到阈值进入老年代 │ │ │ │ │
│ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Survivor Space 1 (S1/To) │ │ │ │ │
│ │ │ │ │ 作用: 与 S0 交替使用 │ │ │ │ │
│ │ │ │ │ 容量: 约占新生代的10% │ │ │ │ │
│ │ │ │ │ GC: 始终保持为空, 作为复制目标 │ │ │ │ │
│ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 新生代特点: │ │ │ │
│ │ │ │ ├── 对象生命周期短, 98% 对象很快被回收 │ │ │ │
│ │ │ │ ├── 使用复制算法 (Copying) │ │ │ │
│ │ │ │ ├── Minor GC 频率高、速度快 │ │ │ │
│ │ │ │ └── 对象年龄阈值: 默认15 (-XX:MaxTenuringThreshold) │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 老年代 (Old Generation) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 作用: 存放存活时间长的对象 │ │ │ │
│ │ │ │ 容量: 约占堆的2/3 │ │ │ │
│ │ │ │ GC: Major GC/Full GC, 使用标记-清除或标记-整理算法 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 老年代特点: │ │ │ │
│ │ │ │ ├── 对象生命周期长, 存活对象多 │ │ │ │
│ │ │ │ ├── 使用标记-清除/标记-整理算法 │ │ │ │
│ │ │ │ ├── Major GC 频率低、速度慢 │ │ │ │
│ │ │ │ └── 大对象直接进入老年代 (-XX:PretenureSizeThreshold) │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 堆特点: │ │ │
│ │ │ ├── 所有线程共享, 最大的一块内存区域 │ │ │
│ │ │ ├── 存放对象实例和数组 │ │ │
│ │ │ ├── GC 的主要战场 │ │ │
│ │ │ └── OOM: java.lang.OutOfMemoryError: Java heap space │ │ │
│ │ │ │ │ │
│ │ │ 参数: │ │ │
│ │ │ ├── -Xms: 堆最小容量 (默认物理内存1/64) │ │ │
│ │ │ ├── -Xmx: 堆最大容量 (默认物理内存1/4) │ │ │
│ │ │ └── -XX:NewRatio: 新生代/老年代比例 (默认2) │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 方法区 (Method Area) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 运行时常量池 (Runtime Constant Pool) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 内容: │ │ │ │
│ │ │ │ ├── 字面量 (字符串、final常量) │ │ │ │
│ │ │ │ ├── 符号引用 (类、方法、字段的全限定名) │ │ │ │
│ │ │ │ └── 直接引用 (解析后的内存地址) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 特点: │ │ │ │
│ │ │ │ ├── Class 文件常量池在类加载时进入 │ │ │ │
│ │ │ │ ├── 动态生成的常量 (如String.intern()) │ │ │ │
│ │ │ │ └── JDK 7+ 字符串常量移至堆中 │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 元空间 (Metaspace) │ │ │ │
│ │ │ │ (JDK 8+) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 内容: │ │ │ │
│ │ │ │ ├── 类信息 (名称、访问修饰符) │ │ │ │
│ │ │ │ ├── 字段信息 (名称、类型、修饰符) │ │ │ │
│ │ │ │ ├── 方法信息 (名称、参数、返回值、字节码) │ │ │ │
│ │ │ │ ├── 常量池 (运行时常量池) │ │ │ │
│ │ │ │ └── 静态变量 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 特点 (JDK 8+ Metaspace vs JDK 7 PermGen): │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ 对比维度 PermGen (JDK 7) Metaspace (JDK 8+) │ │ │ │ │
│ │ │ │ │ ─────────── ────────────── ────────────────── │ │ │ │ │
│ │ │ │ │ 内存位置 堆内存内 本地内存 (Native) │ │ │ │ │
│ │ │ │ │ 大小限制 固定大小 (-XX:MaxPermSize) │ │ │ │ │
│ │ │ │ │ 默认64MB/128MB │ │ │ │ │
│ │ │ │ │ 容易溢出 │ │ │ │ │
│ │ │ │ │ 大小限制 无固定上限, 受系统内存限制 │ │ │ │ │
│ │ │ │ │ 垃圾回收 伴随 Full GC │ │ │ │ │
│ │ │ │ │ 效率低 │ │ │ │ │
│ │ │ │ │ 垃圾回收 类卸载时回收, 独立于 GC │ │ │ │ │
│ │ │ │ │ 效率高 │ │ │ │ │
│ │ │ │ │ 参数 -XX:PermSize/-XX:MaxPermSize │ │ │ │ │
│ │ │ │ │ -XX:MaxPermSize 必须设置 │ │ │ │ │
│ │ │ │ │ 参数 -XX:MetaspaceSize │ │ │ │ │
│ │ │ │ │ -XX:MaxMetaspaceSize │ │ │ │ │
│ │ │ │ │ 默认无限制 │ │ │ │ │
│ │ │ │ │ OOM 消息 java.lang.OutOfMemoryError: │ │ │ │ │
│ │ │ │ │ PermGen space │ │ │ │ │
│ │ │ │ │ OOM 消息 java.lang.OutOfMemoryError: │ │ │ │ │
│ │ │ │ │ Metaspace │ │ │ │ │
│ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 方法区特点: │ │ │
│ │ │ ├── 所有线程共享 │ │ │
│ │ │ ├── 存放已被虚拟机加载的类信息、常量、静态变量等 │ │ │
│ │ │ └── JDK 7及以前叫永久代(PermGen), JDK 8+叫元空间(Metaspace) │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 直接内存 (Direct Memory) │ │ │
│ │ │ │ │ │
│ │ │ 作用: NIO 使用的堆外内存, 通过 DirectByteBuffer 访问 │ │ │
│ │ │ 特点: 不在 JVM 堆内, 受本机物理内存限制 │ │ │
│ │ │ OOM: java.lang.OutOfMemoryError: Direct buffer memory │ │ │
│ │ │ 参数: -XX:MaxDirectMemorySize │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
三、各内存区域详解
3.1 程序计数器(Program Counter)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 程序计数器 │
│ │
│ 📍 作用: │
│ ├── 记录当前线程正在执行的字节码指令地址(行号) │
│ ├── 分支、循环、跳转、异常处理、线程恢复都依赖它 │
│ └── 如果执行 native 方法, 计数器值为空 (Undefined) │
│ │
│ 🎯 特点: │
│ ├── 线程私有(每个线程都有独立的程序计数器) │
│ ├── 占用内存极小, 可忽略不计 │
│ └── JVM 规范中唯一没有规定 OOM 的区域 │
│ │
│ 💡 面试要点: │
│ ├── 为什么是线程私有? │
│ │ → 多线程执行时, 线程切换后需要恢复到正确的执行位置 │
│ │ → 每个线程需要独立的计数器来记录自己的执行位置 │
│ │ │
│ └── 为什么不会 OOM? │
│ → 计数器只需要记录当前指令地址, 大小固定 │
│ → 不会随着程序执行而增长 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 虚拟机栈(Java Virtual Machine Stack)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 虚拟机栈 │
│ │
│ 📍 作用: │
│ ├── 描述 Java 方法执行的内存模型 │
│ ├── 每个方法调用都会创建一个栈帧 (Stack Frame) │
│ └── 栈帧入栈/出栈对应方法的调用/返回 │
│ │
│ 📐 栈帧结构: │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 栈帧 (Stack Frame) │ │
│ │ │ │
│ │ 1. 局部变量表 (Local Variable Table) │ │
│ │ ├── 存储方法参数和局部变量 │ │
│ │ ├── 以 Slot 为单位, 默认 32 位 │ │
│ │ ├── long/double 占用 2 个 Slot │ │
│ │ └── this 引用在实例方法中占据第 0 位 │ │
│ │ │ │
│ │ 2. 操作数栈 (Operand Stack) │ │
│ │ ├── 作为方法执行的临时工作区 │ │
│ │ ├── 存放计算过程中的中间结果 │ │
│ │ └── 方法调用时传递参数 │ │
│ │ │ │
│ │ 3. 动态链接 (Dynamic Linking) │ │
│ │ ├── 指向运行时常量池中该方法的符号引用 │ │
│ │ └── 运行时解析为直接引用 │ │
│ │ │ │
│ │ 4. 方法返回地址 (Return Address) │ │
│ │ ├── 存放调用该方法前的程序计数器值 │ │
│ │ └── 方法执行完后返回到调用者 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 🎯 特点: │
│ ├── 线程私有 │
│ ├── LIFO (后进先出) │
│ ├── 栈深度固定 (由 -Xss 参数控制) │
│ └── 栈帧内存随方法调用分配, 方法返回释放 │
│ │
│ ⚠️ 异常场景: │
│ ├── StackOverflowError: │
│ │ → 原因: 栈深度超过虚拟机允许的最大深度 │
│ │ → 场景: 无限递归调用 │
│ │ → 参数: -Xss 控制栈大小 │
│ │ │
│ └── OutOfMemoryError: │
│ → 原因: 无法创建新的线程栈 │
│ → 场景: 创建大量线程 │
│ → 参数: -Xss 越小, 可创建线程越多 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 本地方法栈(Native Method Stack)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 本地方法栈 │
│ │
│ 📍 作用: │
│ ├── 为 native 方法提供栈空间 │
│ ├── native 方法是用非 Java 语言编写的方法 │
│ └── 如 JNI (Java Native Interface) 调用的 C/C++ 方法 │
│ │
│ 🎯 特点: │
│ ├── 与虚拟机栈类似, 但服务于 native 方法 │
│ ├── 线程私有 │
│ └── Sun HotSpot VM 将其与虚拟机栈合二为一 │
│ │
│ ⚠️ 异常场景: │
│ ├── StackOverflowError: native 方法执行时递归太深 │
│ └── OutOfMemoryError: 无法为新的 native 方法分配栈空间 │
│ │
│ ⚙️ 参数: │
│ └── -Xoss: 设置本地方法栈大小 (HotSpot 中无效) │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 堆(Heap)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 堆 │
│ │
│ 📍 作用: │
│ ├── 存放对象实例和数组 │
│ ├── GC (垃圾回收) 的主要战场 │
│ └── 几乎所有对象都在这里分配内存 │
│ │
│ 📐 分代结构: │
│ │
│ 新生代 (约占堆的1/3): │
│ ├── Eden: 新对象分配区 (约80%) │
│ │ → 大部分对象在这里创建 │
│ │ → Minor GC 时, 存活对象进入 Survivor │
│ │ │
│ ├── S0 (From): 幸存者区0 (约10%) │
│ │ → 存放 Minor GC 后存活的对象 │
│ │ │
│ └── S1 (To): 幸存者区1 (约10%) │
│ → 与 S0 交替使用, 始终保持为空作为复制目标 │
│ │
│ 老年代 (约占堆的2/3): │
│ ├── 存放存活时间长的对象 │
│ ├── 新生代对象年龄达到阈值 (-XX:MaxTenuringThreshold) 后进入 │
│ ├── 大对象直接进入 (-XX:PretenureSizeThreshold) │
│ └── Major GC/Full GC 的主要区域 │
│ │
│ 🗑️ GC 类型: │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ GC 类型 触发条件 回收区域 耗时 │ │
│ │ ────────── ────────── ────────── ────── │ │
│ │ Minor GC Eden 满 新生代 快 │ │
│ │ Major GC 老年代满 老年代 慢 │ │
│ │ Full GC 系统触发 全堆+方法区 很慢 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 🎯 特点: │
│ ├── 所有线程共享 │
│ ├── JVM 中最大的一块内存区域 │
│ ├── 可通过 -Xms/-Xmx 参数调整大小 │
│ └── 对象分配使用指针碰撞或空闲列表算法 │
│ │
│ ⚠️ OOM 场景: │
│ ├── 错误: java.lang.OutOfMemoryError: Java heap space │
│ ├── 原因: 堆内存不足, 无法为新对象分配空间 │
│ └── 场景: │
│ ├── 创建大量大对象 │
│ ├── 内存泄漏 (对象引用未释放) │
│ ├── 无限循环创建对象 │
│ └── 堆设置过小 │
│ │
│ ⚙️ 参数: │
│ ├── -Xms: 堆最小容量 (默认物理内存的1/64) │
│ ├── -Xmx: 堆最大容量 (默认物理内存的1/4) │
│ ├── -XX:NewRatio: 新生代/老年代比例 (默认2) │
│ ├── -XX:SurvivorRatio: Eden/Survivor 比例 (默认8) │
│ └── -XX:MaxTenuringThreshold: 对象晋升年龄阈值 (默认15) │
│ │
└─────────────────────────────────────────────────────────────────┘
3.5 方法区(Method Area)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 方法区 │
│ │
│ 📍 作用: │
│ ├── 存储已被虚拟机加载的类信息 │
│ ├── 存储常量、静态变量、即时编译器编译后的代码 │
│ └── 运行时常量池是方法区的一部分 │
│ │
│ 📐 组成: │
│ │
│ 1. 运行时常量池: │
│ ├── 字面量 (字符串、final常量值) │
│ ├── 符号引用 (类、方法、字段的全限定名) │
│ ├── 直接引用 (解析后的内存地址) │
│ └── JDK 7+ 字符串常量移至堆中 │
│ │
│ 2. 类信息: │
│ ├── 类的全限定名 │
│ ├── 父类的全限定名 │
│ ├── 访问修饰符 (public, abstract, final) │
│ └── 接口列表 │
│ │
│ 3. 字段信息: │
│ ├── 字段名称 │
│ ├── 字段类型 │
│ └── 字段修饰符 │
│ │
│ 4. 方法信息: │
│ ├── 方法名称 │
│ ├── 方法参数列表 │
│ ├── 返回值类型 │
│ ├── 方法修饰符 │
│ └── 方法字节码 │
│ │
│ 🔄 JDK 7 vs JDK 8+ 变化: │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 对比维度 PermGen (JDK 7) Metaspace (JDK 8+) │ │
│ │ ───────────── ────────────── ────────────────── │ │
│ │ 内存位置 堆内存内部 本地内存 (Native) │ │
│ │ 大小限制 固定大小 无固定上限 │ │
│ │ 默认大小 64MB (32位) 受系统内存限制 │ │
│ │ 128MB (64位) │ │
│ │ GC 时机 Full GC 时回收 类卸载时独立回收 │ │
│ │ OOM 消息 PermGen space Metaspace │ │
│ │ 参数 -XX:PermSize -XX:MetaspaceSize │ │
│ │ -XX:MaxPermSize -XX:MaxMetaspaceSize│ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ OOM 场景: │
│ │
│ JDK 7: │
│ ├── 错误: java.lang.OutOfMemoryError: PermGen space │
│ └── 场景: │
│ ├── 大量动态生成类 (如反射、动态代理、CGLIB) │
│ ├── String.intern() 调用过多 │
│ └── 热部署频繁重新加载类 │
│ │
│ JDK 8+: │
│ ├── 错误: java.lang.OutOfMemoryError: Metaspace │
│ └── 场景: │
│ ├── 大量动态生成类 (如反射、动态代理、CGLIB) │
│ ├── ASM 字节码生成工具频繁使用 │
│ └── 框架大量使用注解处理器 │
│ │
│ ⚙️ 参数 (JDK 8+): │
│ ├── -XX:MetaspaceSize: 元空间初始大小 (默认约21MB) │
│ ├── -XX:MaxMetaspaceSize: 元空间最大大小 (默认无限制) │
│ └── -XX:CompressedClassSpaceSize: 压缩类空间大小 (默认1GB) │
│ │
└─────────────────────────────────────────────────────────────────┘
3.6 直接内存(Direct Memory)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 直接内存 │
│ │
│ 📍 作用: │
│ ├── NIO 使用的堆外内存 │
│ ├── 通过 DirectByteBuffer 类访问 │
│ └── 避免 Java 堆和 Native 堆之间的数据复制 │
│ │
│ 🎯 特点: │
│ ├── 不在 JVM 规范定义的运行时数据区域内 │
│ ├── 不属于堆内存, 属于本地内存 │
│ ├── 受本机物理内存限制 │
│ └── 速度比堆内存快 (减少一次数据复制) │
│ │
│ ⚠️ OOM 场景: │
│ ├── 错误: java.lang.OutOfMemoryError: Direct buffer memory │
│ ├── 原因: 系统内存不足, 无法分配直接内存 │
│ └── 场景: │
│ ├── NIO 程序分配大量 DirectByteBuffer │
│ ├── -Xmx 设置过大, 挤占系统内存 │
│ └── 直接内存使用后未及时释放 │
│ │
│ ⚙️ 参数: │
│ └── -XX:MaxDirectMemorySize: 最大直接内存大小 (默认等于 -Xmx) │
│ │
└─────────────────────────────────────────────────────────────────┘
四、OOM 速查表
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ JVM 各区域 OOM 速查表 │
│ │
│ ┌───────┬─────────────────────────────────────┬────────────────────────────────┐ │
│ │ 区域 │ 错误信息 │ 触发场景 │ │
│ ├───────┼─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 程序计数器│ 无(JVM 规范未规定) │ 无 │ │
│ ├───────┼─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 虚拟机栈 │ java.lang.StackOverflowError │ 无限递归调用 │ │
│ │ │ │ 栈帧过大 │ │
│ │ │ java.lang.OutOfMemoryError │ 创建大量线程 │ │
│ ├───────┼─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 本地方法栈│ java.lang.StackOverflowError │ native 方法递归太深 │ │
│ │ │ java.lang.OutOfMemoryError │ 无法分配 native 方法栈空间 │ │
│ ├───────┼─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 堆 │ java.lang.OutOfMemoryError: │ 创建大量大对象 │ │
│ │ │ Java heap space │ 内存泄漏 │ │
│ │ │ │ 无限循环创建对象 │ │
│ │ │ │ 堆设置过小 │ │
│ ├───────┼─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 方法区 │ java.lang.OutOfMemoryError: │ 大量动态生成类 (反射/CGLIB) │ │
│ │ (JDK 7)│ PermGen space │ String.intern() 过多 │ │
│ │ │ │ 热部署频繁 │ │
│ │ ├─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 方法区 │ java.lang.OutOfMemoryError: │ 大量动态生成类 (反射/CGLIB) │ │
│ │ (JDK 8+)│ Metaspace │ ASM 字节码生成 │ │
│ │ │ │ 注解处理器大量使用 │ │
│ ├───────┼─────────────────────────────────────┼────────────────────────────────┤ │
│ │ 直接内存│ java.lang.OutOfMemoryError: │ NIO 分配大量 DirectByteBuffer │ │
│ │ │ Direct buffer memory │ -Xmx 设置过大 │ │
│ │ │ │ 直接内存未及时释放 │ │
│ └───────┴─────────────────────────────────────┴────────────────────────────────┘ │
│ │
│ ┌───────┬──────────────────────────────┬─────────────────────────────────────┐ │
│ │ 区域 │ JVM 参数 │ 修复策略 │ │
│ ├───────┼──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 程序计数器│ 无 │ 无需处理 │ │
│ ├───────┼──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 虚拟机栈 │ -Xss (默认1MB) │ 检查递归深度 │ │
│ │ │ │ 增加 -Xss 值 │ │
│ │ │ │ 减少线程数量 │ │
│ ├───────┼──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 本地方法栈│ -Xoss (HotSpot 无效) │ 检查 native 方法实现 │ │
│ ├───────┼──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 堆 │ -Xms/-Xmx │ 增加堆大小 │ │
│ │ │ -XX:NewRatio │ 排查内存泄漏 │ │
│ │ │ -XX:SurvivorRatio │ 优化对象生命周期 │ │
│ │ │ │ 使用内存分析工具 (MAT/JProfiler) │ │
│ ├───────┼──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 方法区 │ -XX:PermSize/-XX:MaxPermSize │ 增加永久代大小 (JDK 7) │ │
│ │ (JDK 7)│ │ 升级到 JDK 8+ │ │
│ │ │ │ 减少动态类生成 │ │
│ │ ├──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 方法区 │ -XX:MetaspaceSize │ 设置 -XX:MaxMetaspaceSize │ │
│ │ (JDK 8+)│ -XX:MaxMetaspaceSize │ 减少动态类生成 │ │
│ │ │ │ 使用 ClassLoader 缓存 │ │
│ ├───────┼──────────────────────────────┼─────────────────────────────────────┤ │
│ │ 直接内存│ -XX:MaxDirectMemorySize │ 减少 DirectByteBuffer 使用 │ │
│ │ │ │ 及时释放直接内存 │ │
│ │ │ │ 合理设置 -Xmx 大小 │ │
│ └───────┴──────────────────────────────┴─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
五、关键面试问题
5.1 为什么堆要分代?
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 为什么堆要分代? │
│ │
│ 📊 分代依据: │
│ ├── 弱分代假说: 大部分对象朝生夕死 │
│ └── 强分代假说: 熬过多次 GC 的对象更难被回收 │
│ │
│ 🎯 分代优势: │
│ │
│ 1. 提高 GC 效率: │
│ ├── 新生代: 复制算法, 只需要复制少量存活对象 │
│ ├── 老年代: 标记-清除/整理, 减少内存碎片 │
│ └── 分代后, Minor GC 只回收新生代, 速度快 │
│ │
│ 2. 减少停顿时间: │
│ ├── Minor GC 频繁但快速 │
│ ├── Major GC 不频繁但慢 │
│ └── 用户感知主要来自 Major GC │
│ │
│ 3. 针对性优化: │
│ ├── 新生代: 适合复制算法 │
│ └── 老年代: 适合标记-清除/整理算法 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 对象分配流程
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 对象分配流程 │
│ │
│ 1. 检查是否是大对象 │
│ ├── 是 → 直接进入老年代 (-XX:PretenureSizeThreshold) │
│ └── 否 → 进入下一步 │
│ │
│ 2. 检查新生代是否有足够空间 │
│ ├── 有 → 分配到 Eden │
│ └── 否 → 执行 Minor GC │
│ │
│ 3. Minor GC 后检查 │
│ ├── 存活对象能放入 Survivor → 放入 S0/S1 │
│ └── 不能放入 → 通过分配担保机制进入老年代 │
│ │
│ 4. 对象在 Survivor 中复制 │
│ ├── 每次 Minor GC, 对象年龄+1 │
│ ├── 年龄达到阈值 (-XX:MaxTenuringThreshold) → 进入老年代 │
│ └── Survivor 空间不足 → 直接进入老年代 │
│ │
│ 📋 流程图: │
│ │
│ new Object() │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 大对象检查 │──是──→ 老年代 │
│ └──────┬───────┘ │
│ │ 否 │
│ ▼ │
│ ┌──────────────┐ │
│ │ Eden 有空间? │──是──→ 分配到 Eden │
│ └──────┬───────┘ │
│ │ 否 │
│ ▼ │
│ ┌──────────────┐ │
│ │ Minor GC │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 存活对象放入 │──→ 年龄达标? ──是──→ 老年代 │
│ │ Survivor │ │ │
│ └──────────────┘ │ 否 │
│ ▼ │
│ 继续在 Survivor 中复制 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 JDK 7 到 JDK 8 的内存模型变化
┌─────────────────────────────────────────────────────────────────┐
│ │
│ JDK 7 → JDK 8 内存模型变化 │
│ │
│ 📌 主要变化: │
│ │
│ 1. 永久代 (PermGen) → 元空间 (Metaspace) │
│ ├── 内存位置: 堆内 → 本地内存 │
│ ├── 大小限制: 固定 → 无上限 │
│ └── GC 效率: 低 → 高 │
│ │
│ 2. 字符串常量池移至堆中 │
│ ├── JDK 7: 字符串常量在永久代 │
│ ├── JDK 8+: 字符串常量在堆中 │
│ └── 影响: String.intern() 不再导致 PermGen OOM │
│ │
│ 3. 静态变量移至堆中 │
│ ├── JDK 7: 静态变量在永久代 │
│ └── JDK 8+: 静态变量在堆中 (所属类的 Class 对象中) │
│ │
│ 🎯 变化原因: │
│ │
│ 1. 解决永久代 OOM 问题 │
│ ├── 永久代大小固定, 容易溢出 │
│ ├── 元空间使用本地内存, 理论上不会溢出 │
│ │
│ 2. 提高 GC 效率 │
│ ├── 永久代回收需要 Full GC │
│ ├── 元空间可以独立回收, 不影响堆 │
│ │
│ 3. 简化 JVM 实现 │
│ ├── 移除永久代, 减少内存管理复杂性 │
│ └── 统一使用本地内存管理 │
│ │
│ ⚠️ 迁移注意事项: │
│ │
│ ├── 移除 -XX:PermSize/-XX:MaxPermSize 参数 │
│ ├── 添加 -XX:MetaspaceSize/-XX:MaxMetaspaceSize 参数 │
│ ├── String.intern() 行为变化 │
│ └── 热部署方案需要调整 │
│ │
└─────────────────────────────────────────────────────────────────┘
六、实战:各区域 OOM 触发示例
我为你准备了一个专门用于触发各区域 OOM 的示例工程:
6.1 项目结构
jvm-oom-demo/
├── src/main/java/com/example/demo/
│ ├── StackOverflowDemo.java # 栈溢出
│ ├── HeapOOMDemo.java # 堆溢出
│ ├── MetaspaceOOMDemo.java # 元空间溢出
│ ├── DirectMemoryOOMDemo.java # 直接内存溢出
│ └── ThreadOOMDemo.java # 线程过多导致 OOM
└── pom.xml
6.2 触发命令示例
# 1. 栈溢出 (-Xss256k 减小栈大小)
java -Xss256k com.example.demo.StackOverflowDemo
# 2. 堆溢出 (-Xmx32m 减小堆大小)
java -Xmx32m com.example.demo.HeapOOMDemo
# 3. 元空间溢出 (-XX:MaxMetaspaceSize=32m)
java -XX:MaxMetaspaceSize=32m com.example.demo.MetaspaceOOMDemo
# 4. 直接内存溢出 (-XX:MaxDirectMemorySize=32m)
java -XX:MaxDirectMemorySize=32m com.example.demo.DirectMemoryOOMDemo
# 5. 线程过多导致 OOM (-Xss128k 减小每个线程栈大小)
java -Xss128k com.example.demo.ThreadOOMDemo
6.3 代码示例
栈溢出:
public class StackOverflowDemo {
private static int depth = 0;
public static void recursiveCall() {
depth++;
System.out.println("递归深度: " + depth);
recursiveCall();
}
public static void main(String[] args) {
recursiveCall();
}
}
堆溢出:
public class HeapOOMDemo {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int count = 0;
while (true) {
list.add(new byte[1024 * 1024]);
System.out.println("已分配: " + ++count + "MB");
}
}
}
元空间溢出:
public class MetaspaceOOMDemo {
public static void main(String[] args) throws Exception {
int count = 0;
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create();
System.out.println("已创建类: " + ++count);
}
}
}
七、总结
7.1 内存模型核心要点
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 内存模型核心要点 │
│ │
│ 线程私有区域: │
│ ├── 程序计数器: 记录指令地址, 唯一不 OOM │
│ ├── 虚拟机栈: 方法执行栈, 栈帧=局部变量表+操作数栈+动态链接+返回地址│
│ └── 本地方法栈: native 方法栈 │
│ │
│ 线程共享区域: │
│ ├── 堆: 对象分配区, 分新生代(Eden/S0/S1)和老年代 │
│ ├── 方法区: 类信息+常量池, JDK 7 PermGen → JDK 8+ Metaspace │
│ └── 直接内存: NIO 堆外内存 │
│ │
│ OOM 速记: │
│ ├── StackOverflowError: 栈深度超限 (递归太深) │
│ ├── Java heap space: 堆内存不足 │
│ ├── PermGen space: JDK 7 永久代不足 │
│ ├── Metaspace: JDK 8+ 元空间不足 │
│ ├── Direct buffer memory: 直接内存不足 │
│ └── 创建线程失败: 线程栈占用过多内存 │
│ │
│ JDK 8+ 关键参数: │
│ ├── -Xms/-Xmx: 堆大小 │
│ ├── -Xss: 栈大小 │
│ ├── -XX:MetaspaceSize/-XX:MaxMetaspaceSize: 元空间大小 │
│ └── -XX:MaxDirectMemorySize: 直接内存大小 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 面试准备清单
✅ 理解每个内存区域的作用和特点
✅ 掌握栈帧的四个组成部分
✅ 理解堆的分代结构和 GC 流程
✅ 知道 JDK 7 和 JDK 8+ 的内存模型差异
✅ 能说出每个区域的 OOM 场景和原因
✅ 熟悉常用的 JVM 参数
✅ 了解对象分配流程
✅ 知道为什么堆要分代
💡 互动话题:你在面试中遇到过哪些关于 JVM 内存模型的问题?有没有被问到过特别刁钻的问题?欢迎在评论区分享你的经历!
标题:面试必问的 JVM 内存模型,一张图画清楚
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/07/27/1785060795141.html
公众号:服务端技术精选
- 一、引言
- 二、JVM 内存模型全景图
- 三、各内存区域详解
- 3.1 程序计数器(Program Counter)
- 3.2 虚拟机栈(Java Virtual Machine Stack)
- 3.3 本地方法栈(Native Method Stack)
- 3.4 堆(Heap)
- 3.5 方法区(Method Area)
- 3.6 直接内存(Direct Memory)
- 四、OOM 速查表
- 五、关键面试问题
- 5.1 为什么堆要分代?
- 5.2 对象分配流程
- 5.3 JDK 7 到 JDK 8 的内存模型变化
- 六、实战:各区域 OOM 触发示例
- 6.1 项目结构
- 6.2 触发命令示例
- 6.3 代码示例
- 七、总结
- 7.1 内存模型核心要点
- 7.2 面试准备清单
评论
0 评论