JVM 内存模型与对象生命周期

java-concurrency 📚 learning jvm-memory-model · jvm · memory-model · heap · stack · escape-analysis · off-heap · gc-roots

TL;DR(30 秒扫完)

  • Java 对象默认在堆上,但逃逸分析+JIT 可标量替换(消除对象);堆外是主动使用不是自动迁移
  • 存活判定用可达性分析(从 GC Roots 遍历),不用引用计数(循环引用死锁)
  • GC Roots = 栈局部变量 + 静态字段 + 常量池 + synchronized 锁对象 + JNI 引用 + 类加载器 + JVM 内部
  • 对象"不可达"≠"已死":finalize() 可自救(JDK 9 弃用改 Cleaner)
  • 弱分代假说:几乎所有对象朝生夕死,熬过几次 GC 的对象更难死

关键结论

结论 AHotSpot 更常见的是消除对象(标量替换)而非把对象搬到栈;"栈上分配"是修辞
结论 B堆外内存(DirectByteBuffer/Unsafe/Netty PooledAllocator)是独立机制,GC 只回收引用包装对象
结论 CGC 判定存活 = 可达性分析;引用计数因循环引用被 Java/Python/JS 全部放弃
结论 D分代设计的理论基础是弱分代假说——几乎所有对象朝生夕死

完整讲解(费曼四步)

STEP 1 · 概念

JVM 内存模型规定对象语义上属于托管对象,默认在堆上分配。但 JIT 通过逃逸分析+标量替换,可在特定条件下不产生堆分配。堆外内存是另一套独立于 Java 堆的原生内存,需要开发者主动使用。

STEP 2 · 大白话

类比:栈上分配像是"临时工",做完就走;堆分配像"正式员工",由公司(GC)管理;堆外像"外包",你要自己管薪资(释放)。JIT 优化的精髓是让"正式员工"直接以"临时工"的方式干活——把对象拆成局部变量,连"入职手续"(堆分配)都省了。

STEP 3 · 底层

对象分配位置

位置何时分配谁释放典型载体
堆默认路径,newGC(TLAB 指针碰撞)绝大多数对象
栈(标量替换)逃逸分析判定不逃逸方法出栈自动清理JIT 优化后的短命对象
堆外开发者主动申请显式释放 / CleanerDirectByteBuffer、Netty PooledByteBufAllocator
逃逸分析三分类:
分类场景JIT 处理
不逃逸只在创建方法内使用标量替换(首选)/ 栈分配
线程逃逸跨方法调用不出线程TLAB 快速路径
堆逃逸引用保存到全局/返回其他线程必须堆分配
JDK 9+ 逃逸分析默认开启(-XX:+DoEscapeAnalysis),标量替换配套 -XX:+EliminateAllocations。

存活判定:两种算法

算法机制缺陷使用者
引用计数每个对象维护计数器,+1/-1,为 0 死循环引用无法回收(A→B→A)Obj-C ARC(靠 weak 破环)、Go 早期(弃用)
可达性分析从 GC Roots 遍历引用链需 STW 或并发标记Java / Python / JS V8

GC Roots 完整清单(HotSpot)

  • 栈引用:活跃线程的栈帧局部变量、方法参数、运算栈
  • 静态字段:类的 static 变量(含 static final 引用)
  • 常量池引用:运行时常量池的类引用
  • synchronized 持有的对象(监视器锁对象)
  • JNI 引用:本地方法持有的 Java 对象
  • 类加载器 + JVM 内部引用(GC 实现细节、Unreachable class 元数据)

"不可达" ≠ "已死"

  • 若重写 finalize() 且未调用过 → GC 放到 F-Queue,"自救线程"执行
  • 在 finalize() 中重新赋值可回到可达(只有一次机会)
  • JDK 9+ 弃用 finalize(性能差、时机不确定),改用 Cleaner(虚引用+ReferenceQueue)

STEP 4 · 简化

一句话总结:对象默认堆上,JIT 可能消除,堆外自己管;存活判定用可达性,从 GC Roots 出发;分代设计建立在"朝生夕死"假说上。
记忆口诀:
  • 堆为主,标量为辅,堆外自理
  • 可达性 > 引用计数(循环引用是坑)
  • GC Roots 六项:栈、静、常、锁、JNI、类加载

常见误区

"JVM 会自动把大对象搬到堆外"
堆外是独立机制,需要主动使用 DirectByteBuffer/Unsafe/Netty PooledAllocator
"栈上分配就是对象真的放在栈帧里"
HotSpot 更常见是标量替换——把对象字段拆成寄存器/栈变量
"引用计数 Java 也能用"
循环引用 A→B→A 无法回收,Java/Python/JS 全部放弃
"GC Roots 就是静态字段+局部变量"
漏了 synchronized 锁对象、JNI 引用、类加载器、JVM 内部
"不可达 = 已死"
finalize() 可自救;JDK 9+ 改 Cleaner

延伸追问

逃逸分析什么时候失效?
对象被同步(synchronized this)、反射访问、赋值给 volatile、序列化、保存到静态字段、接口调用无法确定最终类型时,JIT 保守放弃标量替换
DirectByteBuffer 如何避免泄漏?
JDK 9+ 用 Cleaner 自动回收;JDK 8 靠 PhantomReference + 手动 clean;监控 -XX:MaxDirectMemorySize、jcmd VM.native_memory、JFR jdk.DirectByteBuffer 事件
如何用 MAT 排查内存泄漏?
Dump 堆 → MAT 打开 → Dominator Tree 找大对象持有者 → Shortest Path to GC Roots 看是不是"不该被引用的强引用"
弱分代假说对 GC 策略的影响?
新生代小快(复制算法),老年代慢少(标记-整理)——这就是"为什么分代"的答案
finalize() 为什么被弃用?
执行时机不确定、性能差(每次 GC 前要跑)、可能"复活"对象;JDK 9 标记 deprecated,改 Cleaner

速查表

对象位置: 堆(默认)/ 栈(标量替换,非真栈)/ 堆外(主动)
逃逸分析: 不逃逸→标量替换 / 线程逃逸→TLAB / 堆逃逸→堆分配
存活判定: 可达性(GC Roots 出发)> 引用计数(循环引用死锁)
GC Roots: 栈 + 静态字段 + 常量池 + synchronized + JNI + 类加载器
自救机制: finalize(弃用)→ Cleaner(JDK 9+)
分代基础: 弱分代假说(朝生夕死)

Anki 候选卡片

Q: HotSpot 栈上分配的本质是什么?
A: 标量替换(把对象字段拆成寄存器/栈变量),不是对象字面放到栈帧
Q: GC Roots 六项清单?
A: 栈局部变量 / 静态字段 / 常量池 / synchronized 锁对象 / JNI 引用 / 类加载器+JVM 内部
Q: 为什么 Java 不用引用计数?
A: 循环引用 A→B→A 无法回收
Q: DirectByteBuffer 泄漏怎么查?
A: JDK 9+ Cleaner 自动、JDK 8 PhantomReference;jcmd VM.native_memory、JFR jdk.DirectByteBuffer
Q: 弱分代假说是什么?
A: 几乎所有对象朝生夕死,熬过几次 GC 的对象更难死

关联题目

  • 《Java中的对象一定在堆上分配内存吗?》— 00java/03JVM/03对象创建与内存分配/, Round 1 Q1, ⭐⭐⭐
  • 《JVM如何判断对象是否存活?》— 00java/03JVM/04垃圾回收(GC)基础/, Round 1 Q3, ⭐⭐⭐

关联知识

对象默认在堆上,JIT 逃逸分析+标量替换可消除对象;存活判定用可达性分析(不用引用计数因循环引用死锁)
栈上分配像临时工;堆像正式员工;堆外像外包——JIT 精髓是让正式员工直接以临时工方式干活
✦ 记 忆 口 诀 ✦
默认堆上+JIT 消除;可达性分析(非引用计数);弱分代假说
关键可视化
对象分配位置
flowchart TB
  O["Java 对象"] --> H["堆<br/>默认 new 路径<br/>GC 回收(TLAB 指针碰撞)"]
  O --> S["栈(标量替换)<br/>逃逸分析判定不逃逸<br/>方法出栈自动清理<br/>JIT 优化短命对象"]
  O --> E["堆外<br/>开发者主动申请<br/>显式释放 / Cleaner<br/>DirectByteBuffer/Netty PooledByteBufAllocator"]
逃逸分析三分类
flowchart LR
  EA["逃逸分析"] --> N["不逃逸<br/>只在创建方法内使用<br/>标量替换(首选)/ 栈分配"]
  EA --> T["线程逃逸<br/>跨方法调用不出线程<br/>TLAB 快速路径"]
  EA --> H["堆逃逸<br/>引用保存到全局/返回其他线程<br/>必须堆分配"]
GC Roots 完整清单
flowchart TB
  R["GC Roots<br/>HotSpot"] --> R1["栈引用<br/>活跃线程栈帧局部变量、方法参数、运算栈"]
  R --> R2["静态字段<br/>类静态变量"]
  R --> R3["常量池<br/>字符串常量池、类常量"]
  R --> R4["synchronized 锁对象<br/>被锁对象"]
  R --> R5["JNI 引用<br/>本地方法引用"]
  R --> R6["类加载器<br/>类引用"]
  R --> R7["JVM 内部<br/>JIT、JFR 等内部引用"]
存活判定两种算法
flowchart LR
  L["存活判定"] --> RC["引用计数<br/>每个对象维护计数器<br/>+1/-1 为 0 死"]
  L --> RA["可达性分析<br/>从 GC Roots 遍历引用链"]
  RC -.缺陷.-> X["循环引用无法回收<br/>A→B→A<br/>Obj-C ARC 靠 weak 破环<br/>Go 早期弃用"]
  RA --> OK["Java/Python/JS V8 全用<br/>需 STW 或并发标记"]
知识关系

⬆️ 前置(Prerequisite)

暂无

🔄 延伸(Extends)

jvm-reference-typesjvm-gc-algorithms

⚡ 对比(Contrast)

四种引用类型— 内存模型讲'对象在哪',引用类型讲'怎么引用'——决定对象回收时机
🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面