小陈的知识图谱
Java 基础L1 基础核心重点

JVM 内存模型与垃圾回收

运行时数据区、GC 算法、垃圾收集器、内存泄漏排查

JVM 运行时数据区域

JVM 在执行 Java 程序时会把管理的内存划分为若干个不同的数据区域,各自承担不同的职责。

┌─────────────────────────────────────────────────┐
│                  JVM 内存结构                      │
├─────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐             │
│  │  线程私有     │  │  线程共享     │             │
│  │ ┌──────────┐ │  │ ┌──────────┐ │             │
│  │ │程序计数器 │ │  │ │  堆内存  │ │             │
│  │ ├──────────┤ │  │ │(Heap)    │ │             │
│  │ │虚拟机栈  │ │  │ │ 新生代   │ │             │
│  │ │(Stack)   │ │  │ │ 老年代   │ │             │
│  │ ├──────────┤ │  │ └──────────┘ │             │
│  │ │本地方法栈│ │  │ ┌──────────┐ │             │
│  │ └──────────┘ │  │ │ 方法区   │ │             │
│  └──────────────┘  │ │(Metaspace)│             │
│                     │ └──────────┘ │             │
│                     └──────────────┘             │
└─────────────────────────────────────────────────┘

堆(Heap)

  • 所有线程共享,是 JVM 管理最大的一块内存区域,存储对象实例和数组。
  • 堆是垃圾回收的主要战场,因此也被称为"GC 堆"。
  • 堆可以处于物理上不连续但逻辑上连续的内存空间中。

堆的分代结构(JDK 8 及以前思路,G1/ZGC 有所不同):

┌──────────────────────────────────────────┐
│                堆内存                      │
│  ┌──────────────┬──────────┬──────────┐  │
│  │   新生代      │          │          │  │
│  │ ┌────┬────┬──┤  老年代   │  元空间   │  │
│  │ │Eden│ S0 │S1│          │          │  │
│  │ └────┴────┴──┤          │          │  │
│  │  Young Gen   │ Old Gen  │ Metaspace│  │
│  └──────────────┴──────────┴──────────┘  │
│  Heap  ≈  1/3       2/3                  │
└──────────────────────────────────────────┘

  • 新生代(Young Generation):约占堆的 1/3。分为 Eden 区(约占 80%)和两个 Survivor 区(各占 10%)。
  • 老年代(Old Generation):存放长期存活的对象,约占堆的 2/3。
  • JDK 8+ 使用元空间(Metaspace)替代永久代,元空间使用本地内存(Native Memory),不再受 JVM 堆大小限制。

虚拟机栈(Stack)

  • 线程私有,生命周期与线程相同。
  • 每个方法被执行时,JVM 都会同步创建一个栈帧(Stack Frame)
  • 栈帧包含以下结构:

┌─────────────────────────┐
│        栈帧              │
├─────────────────────────┤
│  局部变量表              │  ← 基本类型 + 对象引用
├─────────────────────────┤
│  操作数栈                │  ← 字节码指令的工作区
├─────────────────────────┤
│  动态连接                │  ← 指向运行时常量池的方法引用
├─────────────────────────┤
│  方法出口                │  ← 方法返回地址
└─────────────────────────┘

  • 如果线程请求的栈深度超过 JVM 允许的最大深度,抛出 StackOverflowError

方法区(Metaspace / 元空间)

  • 所有线程共享,存储已被 JVM 加载的类信息、常量、静态变量、JIT 编译后的代码缓存。
  • JDK 8 彻底移除永久代(PermGen),替换为元空间(Metaspace)
  • 元空间使用本地内存,默认无上限,可通过 -XX:MaxMetaspaceSize 限制。
  • 字符串常量池在 JDK 7 中被移到了堆中。

本地方法栈(Native Method Stack)

  • 为 JVM 执行 Native 方法服务。
  • HotSpot 中直接将本地方法栈和虚拟机栈合二为一。

程序计数器(Program Counter Register)

  • 线程私有,记录当前线程执行的字节码指令地址。
  • 如果执行 Native 方法,PC 值为空(Undefined)。
  • 是 JVM 规范中唯一不会出现 OOM 的区域。

对象创建与内存分配

对象分配流程

┌──────────────────────────────────────────┐
│              new 指令                      │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│     1. 类加载检查(类是否已加载/解析)      │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│ 2. 分配内存(指针碰撞 / 空闲列表)         │
│    ┌─ TLAB 优先(线程本地分配缓冲区)      │
│    └─ CAS + 失败重试(全局分配)          │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│ 3. 内存空间初始化为零值                    │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│ 4. 设置对象头(Mark Word + 类型指针)      │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│ 5. 执行 <init> 方法                      │
└──────────────────────────────────────────┘

对象内存布局

┌──────────────────────────────────────────┐
│            对象内存布局                     │
├──────────────────────────────────────────┤
│  对象头(Mark Word) 8字节 / 12字节(64位) │  ← 存 hash、GC 分代年龄、锁状态
├──────────────────────────────────────────┤
│  类型指针(Klass Pointer) 4/8 字节        │  ← 指向方法区的类元数据
├──────────────────────────────────────────┤
│  实例数据(Instance Data)                │  ← 真正的字段值
├──────────────────────────────────────────┤
│  对齐填充(Padding)                      │  ← 保证 8 字节对齐
└──────────────────────────────────────────┘

对象访问定位

  • 句柄访问:Java 栈中引用指向句柄池,句柄池再指向对象实例数据和方法区类型数据。
  • 直接指针访问(HotSpot 使用):引用直接指向对象,速度快,但 GC 移动对象时需要更新。

对象存活判定算法

引用计数法(Reference Counting)

  • 每个对象维护一个引用计数器,被引用时 +1,引用失效时 -1。
  • 缺点:无法解决循环引用问题。主流 JVM 未采用。

可达性分析法(Reachability Analysis / Root GC)

  • GC Roots 出发向下搜索引用链,不可达的对象判定为可回收。

GC Roots 包括:

1. 虚拟机栈中引用的对象(局部变量表)

2. 方法区中静态属性引用的对象

3. 方法区中常量引用的对象

4. 本地方法栈中 Native 方法引用的对象

5. 活跃线程(Thread)

GC Roots
    ┌───┴───┐
    │       │
    ▼       ▼
  ObjectA  ObjectB
    │       │
    ▼       ▼
  ObjectC  ObjectD ──→ ObjectE (可达)
                           │
                           ▼
                        ObjectF (可达)

  ObjectG  ←─ ObjectH  ←─ ObjectI (三者均不可达,循环引用)

四种引用类型

引用类型回收时机用途
强引用(Strong)永不回收,OOM 才报错new 对象
软引用(Soft)内存不足时回收缓存
弱引用(Weak)GC 时立即回收ThreadLocal、WeakHashMap
虚引用(Phantom)随时可回收对象回收跟踪(NIO DirectBuffer)

垃圾回收算法

标记-清除(Mark-Sweep)

[初始状态]    [标记存活]    [清除垃圾]
  ■ ■ □ ■ □     ■ ■ □ ■ □     ■ ■   ■ □
  □ ■ ■ □ ■  →  □ ■ ■ □ ■  →   ■ ■   ■
  ■ □ □ ■ ■     ■ □ □ ■ ■     ■     ■ ■
  [■=存活 □=垃圾]     [内存碎片]

  • 优点:实现简单,与存活对象数量无关。
  • 缺点:产生大量内存碎片,分配大对象时可能提前触发 GC。

标记-复制(Mark-Copy)

┌──────────┬──────────┐        ┌──────────┬──────────┐
  │ Eden+S0  │   S1     │        │ Eden+S0 │   S1     │
  │  存活+垃圾 │  空      │   →   │   空     │  存活    │
  └──────────┴──────────┘        └──────────┴──────────┘
  From空间(使用中)  To空间(空闲)    From(清空)   To(存活)

  • 适用:新生代(对象存活率低)。
  • 优点:无内存碎片,分配高效(指针碰撞)。
  • 缺点:空间利用率低(50% 闲置),存活率高时复制成本大。

标记-整理(Mark-Compact)

标记阶段                             整理阶段
  ┌──────────────┐                  ┌──────────────┐
  │ ■ ■ □ ■ □ ■  │    存活对象移动    │ ■ ■ ■ ■ ■    │
  │ ■ □ □ ■ □ ■  │  ─────────────→  │ ■ ■ ■ ■ ■    │
  │ □ ■ ■ □ ■ ■  │    到一端         │ ■ ■ ■ ■ ■    │
  └──────────────┘                  └──────────────┘
                                    [无碎片,连续可用]

  • 适用:老年代(对象存活率高)。
  • 优点:无内存碎片,内存利用率高。
  • 缺点:移动对象需要 Stop The World,吞吐量受影响。

分代收集理论

  • 新生代:标记-复制,因为大部分对象朝生夕死。
  • 老年代:标记-清除 或 标记-整理,因为对象存活率高。
  • 跨代引用:使用记忆集(Remembered Set)和卡表(Card Table)解决。

垃圾收集器详解

收集器发展时间线

Serial  ─→  ParNew  ─→  CMS  ─→  G1  ─→  ZGC  ─→  Shenandoah
 (单线程)  (多线程)    (并发)    (分区)    (超低延迟)

各收集器对比

收集器区域算法并发/并行STW适用场景
Serial新生代复制串行较长单核、小堆、客户端
ParNew新生代复制并行较短配合 CMS
Parallel Scavenge新生代复制并行受控吞吐量优先
Serial Old老年代标记-整理串行较长CMS 后备
Parallel Old老年代标记-整理并行受控吞吐量优先
CMS老年代标记-清除并发很短低延迟(已废弃)
G1整堆标记-整理+复制并发+并行可预测大堆、替代 CMS
ZGC整堆染色指针+并发并发<10ms超大堆、超低延迟

CMS 收集器(Concurrent Mark Sweep)

初始标记  并发标记    重新标记    并发清除
    │        │          │          │
    │    ┌───┴───┐      │    ┌─────┘
    ▼    ▼       ▼      ▼    ▼
  ┌──┬──────────┬──┬──────────┐
  │STW│  并发    │STW│  并发    │
  └──┴──────────┴──┴──────────┘

  • 优点:并发收集,停顿时间短。
  • 缺点
1. 对 CPU 敏感,占用资源。

2. 浮动垃圾(Concurrent Mode Failure)。

3. 标记-清除产生碎片,可能导致 Full GC 退化为 Serial Old。

4. JDK 9 标记为废弃,JDK 14 正式移除。

G1 收集器(Garbage First)

G1 将堆划分为 2048 个 Region(1MB~32MB),优先回收垃圾最多的 Region。

┌────┬────┬────┬────┬────┬────┬────┬────┐
│ E  │ E  │ S  │ H  │ O  │ O  │ E  │ E  │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ E  │ O  │ O  │ E  │ E  │ H  │ H  │ O  │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ S  │ E  │ E  │ O  │ O  │ O  │ E  │ E  │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ O  │ O  │ O  │ E  │ E  │ S  │ H  │ E  │
└────┴────┴────┴────┴────┴────┴────┴────┘
E=Eden  S=Survivor  O=Old  H=Humongous(大对象)

G1 工作流程:

1. 年轻代 GC:并行复制存活对象到 Survivor。

2. 并发标记:与 CMS 类似,标记老年代存活对象。

3. 混合 GC:回收年轻代 + 部分高收益老年代 Region。

4. Full GC:G1 无法回收时退化为串行 Full GC(应尽量避免)。

G1 常用参数

  • -XX:+UseG1GC:启用 G1
  • -XX:MaxGCPauseMillis=200:目标最大停顿时间
  • -XX:G1HeapRegionSize=4M:Region 大小
  • -XX:InitiatingHeapOccupancyPercent=45:触发并发标记的堆占用率

ZGC 收集器

  • JDK 11 实验性引入,JDK 15 正式发布,JDK 21 生产就绪。
  • 核心特性:GC 停顿时间不超过 10ms,与堆大小无关(从 128MB 到 16TB)。
  • 核心技术
1. 染色指针(Colored Pointer):在指针的 42 位地址空间中用 4 位存储状态(Finalizable/Remapped/Markable0/Markable1)。

2. 读屏障(Load Barrier):不写屏障,只使用读屏障,修正引用。

3. 并发处理:几乎所有阶段(标记、转移、重映射)都并发执行。

内存泄漏排查与调优

常见内存泄漏场景

1. 静态集合类长期持有对象

public class Cache {
       private static Map<String, Object> map = new HashMap<>();
       public void put(String key, Object value) {
           map.put(key, value); // 从不清理,导致 OOM
       }
   }
   // 解决:使用 WeakHashMap 或设置最大容量

2. 未关闭的资源

// 错误:忘记 close
   Connection conn = dataSource.getConnection();
   Statement stmt = conn.createStatement();
   ResultSet rs = stmt.executeQuery(sql);
   // 没有 finally 块关闭资源

   // 正确:try-with-resources
   try (Connection conn = dataSource.getConnection();
        Statement stmt = conn.createStatement()) {
       // ...
   }

3. 内部类/匿名类持有外部类引用

class Outer {
       private int[] bigData = new int[100_000_000];
       class Inner {
           void doSomething() {}
       }
       Inner getInner() { return new Inner(); }
   }
   // Outer 对象不会被 GC,因为 Inner 持有了 Outer.this
   // 解决:将 Inner 改为 static 内部类

4. equals() 和 hashCode() 不当

// 只实现了 equals 没有实现 hashCode
   // 放入 HashMap 后修改了参与 hash 的字段,导致无法再次找到

5. String.intern() 滥用

- 大量使用 intern() 会导致字符串常量池膨胀,最终 OOM。

内存泄漏排查工具

工具用途命令示例
jps查看 Java 进程jps -l
jstatGC 统计jstat -gcutil pid 1000
jmap堆转储jmap -dump:live,format=b,file=heap.hprof pid
jstack线程栈jstack pid
jinfoJVM 参数jinfo pid
MAT堆分析分析大对象、GC Root 引用链
Arthas在线诊断trace/watch/ognl

GC 调优常用参数

# 堆设置
-Xms4g -Xmx4g                     # 初始/最大堆大小
-Xmn2g                            # 新生代大小
-XX:SurvivorRatio=8               # Eden:Survivor = 8:1:1

GC 收集器

-XX:+UseG1GC # 使用 G1 -XX:MaxGCPauseMillis=200 # 目标停顿时间 -XX:+UseZGC # 使用 ZGC (JDK 15+)

GC 日志 (JDK 9+)

-Xlog:gc*:gc.log:time,uptime,level -Xlog:gc+heap=debug -Xlog:gc+age=trace

OOM 自动导出堆

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps/

元空间

-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M

GC 调优核心目标

指标说明适合场景
吞吐量(Throughput)用户代码时间 / 总时间批处理、离线计算
延迟(Latency)STW 时间实时系统、Web 服务
内存占用(Footprint)堆大小内存受限环境

三者不可兼得,通常需要在延迟吞吐量之间做权衡。

面试高频 Q&A

Q1: JVM 堆和栈的区别?

  • 堆:所有线程共享,存对象实例,GC 管理,可 OOM。
  • 栈:线程私有,存局部变量/操作数栈,存栈帧,可能 StackOverflow 或 OOM。
  • 堆需要 GC,栈方法结束自动释放。

Q2: 什么时候触发 Full GC?

  • 老年代空间不足
  • 元空间不足(Metaspace)
  • System.gc() 显式调用
  • Concurrent Mode Failure(CMS)
  • Promotion Failed / 晋升担保失败

Q3: 对象什么时候进入老年代?

  • 年龄达到 -XX:MaxTenuringThreshold(默认 15)
  • 大对象直接进入老年代(-XX:PretenureSizeThreshold
  • Survivor 区中同龄对象总和超过 Survivor 一半
  • 动态年龄判定

Q4: G1 相比 CMS 有哪些优势?

  • G1 分区式,可预测停顿时间
  • G1 不会产生内存碎片(标记-整理)
  • G1 的 Full GC 比 CMS 的 Concurrent Mode Failure 影响小
  • G1 自动管理新生代大小,无需手动配置

Q5: 如何判断对象已死?

  • 可达性分析法:从 GC Roots 出发搜索,不可达即死亡。
  • 至少需要两次标记:第一次标记不可达,第二次检查是否有必要执行 finalize()(不推荐使用)。

---

核心要点

  • JVM 内存区域划分与作用
  • GC 算法原理(标记-清除/复制/整理)
  • 常见 GC 收集器对比
  • 内存泄漏排查手段(jstack/jmap/mat)
  • 对象存活判定(引用计数/Root GC)