
JVM 内存模型(运行时数据区)分为线程共享和线程私有两大部分,上图是 JDK 8+ 版本的完整结构。
一、线程共享区域
1. 堆(Heap)
- 作用:存放所有对象实例 和数组
- 参数控制:-Xms(初始大小)、-Xmx(最大大小)
- 分代设计 :
- 年轻代(Young) :新对象优先分配在 Eden 区,Minor GC 后存活对象进入 Survivor 区(S0/S1 通过复制算法交替使用)
- 老年代(Old):对象年龄达到阈值(默认 15)或 Survivor 空间不足时晋升,使用标记-清除/整理算法
- 大对象:超过 -XX:PretenureSizeThreshold 直接进入老年代
2. 元空间(Metaspace)
- JDK 8+ 替代永久代(PermGen) ,使用本地内存(Native Memory)
- 存储:类元数据、常量池、字段/方法数据、静态变量
- 参数:-XX:MaxMetaspaceSize 控制上限
- OOM 场景:动态生成大量类(如 CGLIB 代理、反射)
- 直接内存(Direct Memory)
- NIO 使用(ByteBuffer.allocateDirect())
- 通过 Unsafe.allocateMemory() 分配,不受 JVM 堆大小限制
- 参数:-XX:MaxDirectMemorySize
二、线程私有区域
| 区域 | 作用 | 特点 |
|---|---|---|
| 虚拟机栈(VM Stack) | 存储栈帧(局部变量表、操作数栈、动态链接、方法返回地址) | 每个方法调用对应一个栈帧;-Xss 控制栈大小 |
| 本地方法栈(Native Stack) | 为 Native 方法(C/C++)服务 | HotSpot 中与虚拟机栈合并实现 |
| 程序计数器(PC Register) | 记录当前线程执行的字节码行号 | 唯一不会 OOM 的区域;线程切换时恢复执行位置 |
三、对象从创建到销毁的完整生命周期
1. 对象生命周期总览

2. 每个阶段的详细流程
① 类加载检查
java
Object obj = new Object(); // 触发类加载
| 步骤 | 内容 |
|---|---|
| 检查类是否已加载 | 在方法区(元空间)查找该类的类型信息 |
| 未加载则触发类加载 | 加载 → 验证 → 准备 → 解析 → 初始化 |
| 检查访问权限 | private/protected/public 是否允许当前上下文访问 |
② 内存分配
这是最关键的一步,JVM 有三种分配策略:
策略 A:堆上分配(最常用)
java
// 绝大多数对象在堆上分配
Person p = new Person(); // 在 Eden 区分配
分配流程:

TLAB 详解:
| 特性 | 说明 |
|---|---|
| 全称 | Thread Local Allocation Buffer |
| 作用 | 每个线程在 Eden 区预先分配一小块私有内存,避免多线程竞争 |
| 大小 | 默认 Eden 的 1%(-XX:TLABWasteTargetPercent) |
| 优势 | 无锁分配,性能极高 |
| 溢出 | TLAB 放不下时,回退到 Eden 区 CAS 分配 |
策略 B:栈上分配(逃逸分析优化)
java
public void method() {
Person p = new Person(); // p 不会逃逸出方法
p.sayHello();
} // p 随栈帧销毁,无需 GC
逃逸分析(Escape Analysis):
JVM 判断对象是否"逃逸"出方法/线程:
├─ 不逃逸 → 栈上分配(随栈帧弹出自动销毁)
├─ 线程逃逸 → 堆上分配(但可能标量替换)
└─ 全局逃逸 → 必须在堆上分配
标量替换(Scalar Replacement):
java
// 如果 Person 对象不逃逸,JVM 可能不创建对象
// 而是直接将其字段拆分为局部变量
Person p = new Person();
p.name = "Tom"; // → 直接变成 String name = "Tom";
p.age = 20; // → 直接变成 int age = 20;
开启参数:
- -XX:+DoEscapeAnalysis(JDK 8+ 默认开启)
- -XX:+EliminateAllocations(标量替换,默认开启)
策略 C:大对象直接进入老年代
java
// 超过 -XX:PretenureSizeThreshold 的大数组/对象
byte[] bigArray = new byte[1024 * 1024 * 10]; // 10MB,直接进入老年代
③ 对象初始化
内存分配完成后,JVM 执行三步初始化:
┌─────────────────┐
│ 1. 零值初始化 │ 所有字段设为默认值(int=0, boolean=false, ref=null)
│ 2. 设置对象头 │ 设置 Mark Word、Klass Pointer、数组长度
│ 3. 执行 │ 执行构造方法(字段赋值、代码块、构造函数)
└─────────────────┘
对象头(Object Header)结构:

④ 引用建立
java
Person p = new Person(); // p 是栈帧中的局部变量,指向堆中对象
引用关系:

⑤ 对象使用
对象被引用期间,可以被:
- 读取/修改字段
- 调用方法
- 作为参数传递
- 被其他对象引用
⑥ 引用失效
引用失效的几种情况:
| 场景 | 说明 |
|---|---|
| 作用域结束 | 方法执行完毕,栈帧弹出,局部变量引用消失 |
| 显式置 null | p = null; |
| 重新赋值 | p = new Person(); 旧对象引用断开 |
| 容器清理 | list.remove(obj)、map.clear() |
| 弱引用回收 | WeakReference 被 GC 时回收 |
| 软引用回收 | SoftReference 内存不足时回收 |
| 虚引用 | PhantomReference 仅用于跟踪回收 |
⑦ 可达性分析(判断垃圾)
JVM 使用 可达性分析算法(Reachability Analysis) ,而非引用计数:
GC Roots(根对象集合):
├─ 虚拟机栈中局部变量引用的对象
├─ 方法区中静态变量引用的对象
├─ 方法区中常量引用的对象
├─ 本地方法栈中 JNI 引用的对象
├─ JVM 内部的引用(基本数据类型对应的 Class 对象)
├─ 所有被同步锁(synchronized)持有的对象
└─ 反映 JVM 内部情况的 JMXBean、JVMTI 中注册的回调等

⑧ GC 回收
根据对象所在区域,触发不同的 GC:
年轻代回收(Minor GC / Young GC)
触发条件:Eden 区空间不足
回收流程:
- STW(Stop The World)暂停所有应用线程
- 标记 Eden + From Survivor 中的存活对象
- 复制存活对象到 To Survivor
- 清空 Eden + From Survivor
- From 和 To 交换角色
- 对象年龄 +1
- 年龄达到阈值(默认15)→ 晋升老年代
- 恢复应用线程
老年代回收(Major GC / Old GC)
触发条件:
├─ 老年代空间不足
├─ 空间分配担保失败
└─ CMS 的并发标记周期
回收算法:
├─ CMS:标记-清除(并发低停顿,但碎片多)
├─ G1:标记-整理 + 复制(Region 化,可预测停顿)
└─ ZGC/Shenandoah:并发整理(几乎零停顿)
Full GC
触发条件:
├─ 老年代空间不足且无法分配
├─ 元空间不足
├─ System.gc() 调用(建议性)
└─ CMS 的并发模式失败(Concurrent Mode Failure)
特点:回收整个堆(年轻代 + 老年代 + 元空间),停顿时间长
⑨ 内存释放
对象被 GC 回收后:
├─ 内存被标记为空闲
├─ 空闲内存加入空闲列表(Free List)
├─ 下次分配时复用
└─ 长期运行后可能触发内存整理(Compact)消除碎片
内存整理(Compact):
整理前(标记-清除后):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│存活│空闲│存活│空闲│存活│空闲│空闲│存活│ ← 大量碎片
└────┴────┴────┴────┴────┴────┴────┴────┘
整理后(标记-整理):
┌────┬────┬────┬────┬────────────────────┐
│存活│存活│存活│存活│ 连续空闲空间 │ ← 无碎片
└────┴────┴────┴────┴────────────────────┘
3. 完整流程图(文字版)
| 阶段 | 核心内容 |
|---|---|
| 类加载检查 | 双亲委派模型、加载→验证→准备→解析→初始化 |
| 逃逸分析 | 栈上分配、标量替换、不逃逸/线程逃逸/全局逃逸 |
| 内存分配 | TLAB 快速分配、Eden CAS 分配、大对象直接进入老年代 |
| 零值初始化 | 所有字段自动设默认值 |
| 设置对象头 | Mark Word、Klass Pointer、数组长度 |
| 执行构造方法 | <init>() 的完整执行顺序 |
| 引用建立 | 栈帧局部变量 → 堆对象,四种引用类型 |
| 对象使用 | 读写字段、调用方法、同步锁 |
| 引用失效 | 作用域结束、置 null、容器清理、弱/软引用回收 |
| 可达性分析 | GC Roots、两次标记、finalize() 复活机制 |
| GC 回收 | Minor GC(复制算法)、Major GC(CMS/G1/ZGC)、Full GC |
| 内存释放 | 空闲列表、内存整理(Compact) |
4. 关键参数汇总
| 参数 | 作用 |
| ------------------------------ | ---------------------------- |
| -Xms / -Xmx | 堆初始/最大大小 |
| -Xmn / -XX:NewRatio | 年轻代大小 / 年轻代:老年代比例 |
| -XX:SurvivorRatio | Eden : Survivor 比例(默认 8:1:1) |
| -XX:MaxTenuringThreshold | 晋升老年代年龄阈值(默认 15) |
| -XX:PretenureSizeThreshold | 大对象直接进入老年代的阈值 |
| -XX:+UseTLAB | 开启 TLAB(默认开启) |
| -XX:TLABWasteTargetPercent | TLAB 占 Eden 的比例 |
| -XX:+DoEscapeAnalysis | 开启逃逸分析(默认开启) |
| -XX:+EliminateAllocations | 开启标量替换(默认开启) |
| -XX:+UseG1GC / -XX:+UseZGC | 使用 G1 / ZGC 收集器 |
四、关键对比
| 特性 | JDK 7 及以前 | JDK 8+ |
|---|---|---|
| 方法区实现 | 永久代(PermGen),在堆内 | 元空间(Metaspace),在本地内存 |
| 字符串常量池 | 永久代 | 堆 |
| 静态变量 | 永久代 | 元空间 |
| OOM 风险 | PermGen space |
Metaspace |
五、常见 OOM 场景
| 异常类型 | 发生区域 | 典型原因 |
|---|---|---|
Java heap space |
堆 | 内存泄漏、大对象、堆设置过小 |
GC overhead limit exceeded |
堆 | 98% 时间用于 GC,回收不足 2% 空间 |
Metaspace |
元空间 | 动态生成大量类、类加载器未释放 |
Unable to create new native thread |
虚拟机栈 | 线程数超过系统限制 |
Direct buffer memory |
直接内存 | NIO 使用不当、未释放 DirectByteBuffer |