《深入理解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 触发:老年代空间不足
  • 对象分配策略
    • 优先 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 指令)

  1. 类加载检查
    → 检查类是否已加载、解析、初始化(若未,则触发类加载)

  2. 分配内存

    • 指针碰撞(Bump the Pointer):堆规整(Serial、ParNew)
    • 空闲列表(Free List):堆不规整(CMS)
    • 线程安全:TLAB(Thread Local Allocation Buffer)或 CAS + 重试
  3. 初始化零值
    → 所有字段设为默认值(int=0, boolean=false 等)

  4. 设置对象头

    • 对象所属类(Class 指针)
    • 哈希码(lazy compute)
    • GC 分代年龄
    • 锁状态(偏向锁、轻量级锁等)
  5. 执行 <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 线程数超系统限制 jstackulimit -u
Direct buffer memory NIO 直接内存超限 检查 ByteBuffer.allocateDirect() 使用

模拟堆 OOM 示例

public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次 1MB
}
}
}

启动参数:

-Xmx20m -XX:+HeapDumpOnOutOfMemoryError

→ 生成 java_pid.hprof,用 MAT 分析 GC Roots 引用链。


七、高频面试题速记

  1. 哪些区域线程私有?哪些共享?

    • 私有:程序计数器、虚拟机栈、本地方法栈
    • 共享:堆、方法区
  2. StackOverflowError 和 OutOfMemoryError 的区别?

    • SOF:栈深度超限(如无限递归)
    • OOM:无法申请新内存(堆、栈扩展失败、元空间满等)
  3. 方法区在 JDK8 之后叫什么?
    元空间(Metaspace),使用本地内存

  4. 对象一定分配在堆上吗?
    → 不一定!JVM 可能通过 逃逸分析 + 标量替换 将对象分配在栈上(栈上分配)


八、本章核心总结

  • 内存模型是理解 GC、性能调优、OOM 排查的基础
  • 堆是 GC 主战场,方法区存储类元数据
  • 对象生命周期 = 创建 → 使用 → 回收
  • OOM 本质 = 内存不足 or 对象无法释放(泄漏)
  • **调优关键:合理设置堆、元空间、栈大小,选择合适 GC 算法