自动内存管理
《深入理解Java虚拟机》第三版
第二章 自动内存管理 —— 知识笔记
一、JVM运行时数据区域划分
JVM在执行Java程序时,会将内存划分为若干个运行时数据区。根据《Java虚拟机规范》,主要分为以下 5 + 1 个区域:
| 区域 | 线程共享/私有 | 功能说明 | 是否可能 OOM |
|---|---|---|---|
| 程序计数器 | 私有 | 记录当前线程执行的字节码行号(本地方法时为 undefined) | ❌ 不会OOM |
| Java虚拟机栈 | 私有 | 存储栈帧(局部变量表、操作数栈、动态链接、返回地址) | ✅ StackOverflowError / OutOfMemoryError |
| 本地方法栈 | 私有 | 为 Native 方法服务(如 C/C++) | ✅ 同上 |
| Java堆(Heap) | 共享 | 几乎所有对象实例和数组分配于此;GC 主战场 | ✅ OutOfMemoryError: Java heap space |
| 方法区(Method Area) | 共享 | 存储类元信息、常量、静态变量、JIT 编译代码 | ✅ JDK8+ 为 Metaspace OOM |
| 直接内存(Direct Memory) | 非JVM区域 | 通过 ByteBuffer.allocateDirect() 分配堆外内存 |
✅ 受 -XX:MaxDirectMemorySize 限制 |
二、各区域详解
1. 程序计数器(Program Counter Register)
- 作用:线程切换后恢复执行位置。
- 特点:线程私有、无 GC、无 OOM。
2. Java 虚拟机栈
- 每个方法调用对应一个 栈帧(Frame) 入栈。
- 栈帧结构:
- 局部变量表(Slot 存储基本类型、引用)
- 操作数栈
- 动态链接(指向运行时常量池)
- 方法返回地址
- 异常:
StackOverflowError:递归过深OutOfMemoryError:线程过多导致栈总内存超限
- 调优参数:
-Xss1m(默认通常为 1MB)
3. 本地方法栈
- 与虚拟机栈类似,但服务于 Native 方法。
- HotSpot 中常与 Java 栈合并实现。
4. Java 堆(重点!)
- 分代设计:
- 新生代(Young Gen):Eden + Survivor0 + Survivor1(默认 8:1:1)
- Minor GC 触发条件:Eden 区满
- 老年代(Old Gen)
- Major GC / Full GC 触发:老年代空间不足
- 新生代(Young Gen):Eden + Survivor0 + Survivor1(默认 8:1:1)
- 对象分配策略:
- 优先 Eden
- 大对象直接进老年代(避免复制开销)
- 动态年龄判定:Survivor 中相同年龄对象 > 50% → 晋升老年代
- OOM 场景:内存泄漏 or 堆太小
- 调优参数:
-Xms4g -Xmx4g # 固定堆大小
-XX:NewRatio=2 # 老年代:新生代 = 2:1
5. 方法区(JDK8+ 为 Metaspace)
- 存储:类结构、字段/方法数据、静态变量、运行时常量池、JIT 代码
- 演进:
- JDK7-:永久代(PermGen)→ 易 OOM
- JDK8+:元空间(Metaspace)→ 使用本地内存
- OOM 原因:动态生成大量类(如 CGLib、Spring AOP)
- 调优参数:
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m # 默认无上限
6. 运行时常量池(Runtime Constant Pool)
- 方法区的一部分
- 存放编译期生成的字面量(如
"hello")和符号引用 - **JDK7+**:字符串常量池移至 堆中
7. 直接内存
- 非 JVM 管理,但常被使用(NIO)
- 通过
Unsafe.allocateMemory()分配 - OOM:超过
-XX:MaxDirectMemorySize(默认等于-Xmx)
三、对象的创建过程(new 指令)
类加载检查
→ 检查类是否已加载、解析、初始化(若未,则触发类加载)分配内存
- 指针碰撞(Bump the Pointer):堆规整(Serial、ParNew)
- 空闲列表(Free List):堆不规整(CMS)
- 线程安全:TLAB(Thread Local Allocation Buffer)或 CAS + 重试
初始化零值
→ 所有字段设为默认值(int=0, boolean=false 等)设置对象头
- 对象所属类(Class 指针)
- 哈希码(lazy compute)
- GC 分代年龄
- 锁状态(偏向锁、轻量级锁等)
执行
<init>构造方法
→ Java 层面的对象初始化完成
四、对象的内存布局
| 组成 | 内容 |
|---|---|
| 对象头(Header) | Mark Word(哈希、GC年龄、锁标志)、Class 指针 |
| 实例数据(Instance Data) | 用户定义的字段(按 JVM 字段重排序优化) |
| 对齐填充(Padding) | 补齐至 8 字节倍数(HotSpot 要求) |
五、对象的访问定位
JVM 通过 reference(引用)访问对象,有两种方式:
| 方式 | 描述 | 优缺点 |
|---|---|---|
| 句柄访问 | reference → 句柄池 → [对象地址 + 类型信息] | 优点:GC 移动对象只需改句柄;缺点:两次寻址 |
| 直接指针(主流) | reference → 对象地址(对象头含 Class 指针) | 优点:速度快;缺点:GC 需更新所有引用 |
HotSpot 使用 直接指针。
六、内存溢出(OOM)实战与排查
| OOM 类型 | 原因 | 排查工具 |
|---|---|---|
Java heap space |
对象太多 or 内存泄漏 | MAT、JProfiler、jmap -dump |
Metaspace |
动态类加载过多 | jstat -class、检查代理/反射 |
Unable to create new native thread |
线程数超系统限制 | jstack、ulimit -u |
Direct buffer memory |
NIO 直接内存超限 | 检查 ByteBuffer.allocateDirect() 使用 |
模拟堆 OOM 示例:
public class HeapOOM { |
启动参数:
-Xmx20m -XX:+HeapDumpOnOutOfMemoryError |
→ 生成 java_pid.hprof,用 MAT 分析 GC Roots 引用链。
七、高频面试题速记
哪些区域线程私有?哪些共享?
- 私有:程序计数器、虚拟机栈、本地方法栈
- 共享:堆、方法区
StackOverflowError 和 OutOfMemoryError 的区别?
- SOF:栈深度超限(如无限递归)
- OOM:无法申请新内存(堆、栈扩展失败、元空间满等)
方法区在 JDK8 之后叫什么?
→ 元空间(Metaspace),使用本地内存对象一定分配在堆上吗?
→ 不一定!JVM 可能通过 逃逸分析 + 标量替换 将对象分配在栈上(栈上分配)
八、本章核心总结
- 内存模型是理解 GC、性能调优、OOM 排查的基础
- 堆是 GC 主战场,方法区存储类元数据
- 对象生命周期 = 创建 → 使用 → 回收
- OOM 本质 = 内存不足 or 对象无法释放(泄漏)
- **调优关键:合理设置堆、元空间、栈大小,选择合适 GC 算法
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 XXDBOOM!

