Skip to content

Chapter 20 · JVM 内存模型与垃圾回收

程序"自动管理内存"的秘密 — Java 工程师的内功。


🎯 本章目标

  • 理解 JVM 运行时数据区(堆 / 栈 / 方法区 ...)
  • 区分 栈和堆 存什么
  • 理解 垃圾回收(GC) 基本原理
  • 知道常见 GC 算法(标记-清除 / 复制 / 标记-整理)
  • 了解 JVM 调优 的常见参数

1. JVM 是什么?

Java Virtual Machine — 运行 .class 文件的虚拟计算机

.java → javac → .class → JVM → 真实 CPU

JVM 屏蔽了 OS 和 CPU 的差异,是 Java 跨平台的核心。


2. JVM 运行时数据区

JVM 内存结构

┌─────────────────────────────────────────────────┐
│                  方法区(Method Area)           │  ← 存类信息、常量、静态变量
│              JDK 8+ 改名"元空间 Metaspace"        │     线程共享
├─────────────────────────────────────────────────┤
│                    堆(Heap)                    │  ← 存对象实例
│              新生代 + 老年代 + 永久代             │     线程共享,GC 主战场
├─────────────────────────────────────────────────┤
│                  栈(Stack)                     │  ← 存方法的局部变量
│           每个线程一个栈,栈帧记录方法调用         │     线程私有
├─────────────────────────────────────────────────┤
│             程序计数器(PC Register)             │  ← 当前线程执行的字节码行号
├─────────────────────────────────────────────────┤
│              本地方法栈(Native Stack)           │  ← native 方法(JNI)
└─────────────────────────────────────────────────┘

各区域详解

区域线程存什么异常
堆(Heap)共享对象实例、数组OutOfMemoryError
方法区/Metaspace共享类元数据、常量池、静态字段OutOfMemoryError
虚拟机栈(Stack)私有栈帧(局部变量、操作数、引用)StackOverflowError
程序计数器(PC)私有当前指令地址唯一不会 OOM
本地方法栈私有native 方法栈StackOverflowError

3. 栈与堆:经典误解

java
public void demo() {
    int x = 10;                    // 栈:基本类型直接存值
    String s = "hello";            // 栈:引用    堆:String 对象
    Person p = new Person();       // 栈:引用    堆:Person 对象
    int[] arr = {1, 2, 3};         // 栈:引用    堆:数组对象
}

栈与堆

💡 关键

  • 基本类型局部变量 在栈上
  • 对象 一律在堆上
  • 栈上只有"对象的地址(引用)"

栈帧(Stack Frame)

每次方法调用 → 压一个新栈帧;方法结束 → 弹出。栈帧含:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 返回地址
java
void a() { b(); }       // a 的栈帧上面叠 b 的栈帧
void b() { c(); }       // b 上面叠 c

栈过深 → StackOverflowError(无限递归经典错误)。


4. 堆的分代结构

堆分代

                  Heap
       ┌──────────────────────────────┐
       │  新生代 (Young Gen) 1/3       │
       │  ┌──────┬──────┬──────┐      │
       │  │ Eden │  S0  │  S1  │      │  (8:1:1)
       │  └──────┴──────┴──────┘      │
       ├──────────────────────────────┤
       │  老年代 (Old Gen) 2/3         │
       └──────────────────────────────┘

对象的一生

  1. new 时分配在 Eden 区
  2. Eden 满 → 触发 Minor GC
    • 存活对象进入 Survivor 区(S0/S1 来回拷贝)
    • 每熬过一次 GC,年龄 +1
  3. 年龄到 15 岁 → 进入 老年代
  4. 老年代满 → 触发 Full GC("Stop The World",慢且讨厌)

💡 设计哲学大部分对象都是"朝生夕死",只有少数会活到老年代。


5. 垃圾回收:怎么判断垃圾?

5.1 引用计数法(Java 不用)

每个对象一个计数器,引用 +1,失效 -1,归 0 → 垃圾。 问题:循环引用搞不定(A → B → A)。

5.2 可达性分析(Java 用)⭐

GC Roots 出发,能到达的对象都活着,到不了的就是垃圾。

GC Roots 包括

  • 栈帧中的局部变量
  • 方法区中的静态字段
  • 方法区中的常量
  • 本地方法栈中的引用

GC Roots


6. 三种核心 GC 算法

6.1 标记-清除(Mark-Sweep)

  • 标记所有可达对象
  • 清除未标记的
  • 缺点:产生内存碎片

6.2 复制(Copying)

  • 把存活对象复制到另一半
  • 适合存活率低的区域(新生代
  • 缺点:浪费一半空间

6.3 标记-整理(Mark-Compact)

  • 标记后把存活对象往一端移动
  • 适合存活率高的区域(老年代
  • 缺点:移动开销
算法速度空间碎片用于
标记-清除不浪费CMS
复制浪费新生代
标记-整理不浪费老年代

7. 主流垃圾回收器(简表)

收集器适用场景特点
Serial单线程客户端简单,STW 长
Parallel注重吞吐量多线程,JDK 8 默认
CMS关注延迟(已废弃)老年代并发标记清除
G1JDK 9+ 默认分区,低延迟,可预测
ZGCJDK 11+超低延迟(< 10ms)
ShenandoahRH OpenJDK与 ZGC 类似

8. 常见 OOM 错误

错误原因排查
java.lang.OutOfMemoryError: Java heap space堆溢出内存泄漏 / 堆太小
OutOfMemoryError: Metaspace元空间溢出类太多(动态生成代理)
StackOverflowError栈溢出递归过深
OutOfMemoryError: Direct buffer memory直接内存NIO 用太多
OutOfMemoryError: GC overhead limit exceededGC 占 98% 时间没回收 2% 内存内存严重不足

9. JVM 调优常用参数

bash
java \
  -Xms512m       \    # 初始堆大小
  -Xmx2g         \    # 最大堆大小
  -Xmn512m       \    # 新生代大小
  -Xss256k       \    # 每个线程栈大小
  -XX:MetaspaceSize=128m \
  -XX:MaxMetaspaceSize=256m \
  -XX:+UseG1GC   \    # 用 G1 收集器
  -XX:MaxGCPauseMillis=200 \
  -XX:+PrintGCDetails  \    # 打印 GC 日志
  -XX:+HeapDumpOnOutOfMemoryError \    # OOM 时 dump
  -XX:HeapDumpPath=/tmp/heap.hprof \
  -jar app.jar

💡 一般生产环境 -Xms = -Xmx,避免动态扩展抖动。


10. 排查工具

工具用途
jps列出 Java 进程
jstat -gc <pid>看 GC 统计
jstack <pid>看线程栈(死锁排查)
jmap -dump:format=b,file=h.hprof <pid>堆 dump
VisualVM / JConsole图形化监控
Arthas阿里开源,在线诊断神器
MAT (Eclipse Memory Analyzer)分析 hprof

11. 实战练习

文件内容
StackOverflowDemo.java制造 StackOverflowError
HeapOOMDemo.java制造 Java heap space OOM
MemoryLeakDemo.java内存泄漏经典场景
GCDemo.java观察 Minor GC / Full GC

⚠️ 这些 demo 可能崩溃!适合当场看,不要在生产跑。


12. 浏览器演示

打开 demo.html

  • JVM 内存结构交互图
  • 对象的一生动画
  • GC Roots 可达性分析

13. 面试可能会问什么?

Q1: JVM 内存结构?

5 大区域:堆、方法区、虚拟机栈、本地方法栈、程序计数器。 线程共享:堆 + 方法区 线程私有:虚拟机栈 + 本地方法栈 + PC

Q2: 栈和堆什么区别?

存什么局部变量、引用对象实例
分配自动new
速度
线程私有共享
生命周期跟随方法GC 决定

Q3: 怎么判断对象是垃圾?

可达性分析:从 GC Roots 出发能到达的对象活着,否则是垃圾。

GC Roots 主要是:栈中局部变量、静态字段、常量。

Q4: 为什么不用引用计数?

循环引用问题:A.field = B; B.field = A;,但谁也不引用 A、B → 计数永远不为 0,无法回收。

Q5: Minor GC、Major GC、Full GC 区别?

  • Minor GC:清理新生代,频率高,速度快
  • Major GC:清理老年代
  • Full GC:清理整个堆 + 方法区,慢且 STW,要尽量避免

Q6: 什么是 STW?

Stop The World — GC 时所有应用线程被暂停。 应用对延迟敏感时(如交易系统),STW 长就是灾难。 现代 GC(G1 / ZGC)大量并发执行,把 STW 降到几毫秒。

Q7: 老年代为什么不用复制算法?

老年代对象存活率高,复制开销大,浪费一半空间也亏。 所以用 标记-清除标记-整理

Q8: 字符串常量池在哪?

  • JDK 6 及以前:方法区(永久代)
  • JDK 7+
  • JDK 8:方法区 → 元空间(在本地内存里),但常量池仍在堆

Q9: 怎么排查 OOM?

  1. -XX:+HeapDumpOnOutOfMemoryError 自动 dump
  2. 用 MAT / JProfiler 打开 .hprof 看
  3. 找"占内存最多的对象 + 引用链"
  4. 常见:缓存无限增长、ThreadLocal 没 remove、大对象未释放

Q10: G1 和 CMS 区别?

CMSG1
分代不严格分代
区域连续划分成多个 Region
算法标记-清除(碎片)复制 + 标记-整理(无碎片)
延迟控制不可控可设 -XX:MaxGCPauseMillis
状态JDK 9 废弃JDK 9+ 默认

🎁 本章小结

✅ JVM 5 大区域:堆 / 方法区 / 栈 / PC / 本地栈
✅ 对象在堆,引用在栈
✅ 堆分代:新生代(Eden + S0 + S1)+ 老年代
✅ GC 算法:标记-清除 / 复制 / 标记-整理
✅ 可达性分析判断垃圾,从 GC Roots 出发
✅ G1 是现代默认,ZGC 是低延迟之王
✅ 调优看场景:吞吐量 vs 延迟

🎓 通关恭喜!

恭喜你完成了 20 章 Java 学习之旅!🎉

接下来推荐:

  1. JDK 源码阅读:HashMap、ConcurrentHashMap、ThreadPoolExecutor
  2. Spring 框架:Spring Boot、Spring Cloud
  3. 数据库:MySQL + 索引原理 + 事务
  4. 中间件:Redis、Kafka、RabbitMQ
  5. 分布式系统:CAP、一致性算法、微服务

🔗 导航