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 · 底层
对象分配位置
| 位置 | 何时分配 | 谁释放 | 典型载体 |
|---|---|---|---|
| 堆 | 默认路径,new | GC(TLAB 指针碰撞) | 绝大多数对象 |
| 栈(标量替换) | 逃逸分析判定不逃逸 | 方法出栈自动清理 | JIT 优化后的短命对象 |
| 堆外 | 开发者主动申请 | 显式释放 / Cleaner | DirectByteBuffer、Netty PooledByteBufAllocator |
| 分类 | 场景 | JIT 处理 |
|---|---|---|
| 不逃逸 | 只在创建方法内使用 | 标量替换(首选)/ 栈分配 |
| 线程逃逸 | 跨方法调用不出线程 | TLAB 快速路径 |
| 堆逃逸 | 引用保存到全局/返回其他线程 | 必须堆分配 |
-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.DirectByteBufferQ: 弱分代假说是什么?
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)
暂无
🎯 概念
📏 规则
⚠️ 误区
🔍 追问
✨ 口诀
共 0 张卡,点击翻面