JVM GC 与内存管理
10 题 · 平均 2.8/5 · low 2.25 · medium 3 · high 3.5 · 第 1 轮
**核心考点定位**:考察对 JVM 内存模型与 JIT 优化机制(特别是逃逸分析)的深层理解,以及堆内堆外内存的边界认知。
**知识点拆解**:
1. **默认情况:对象在堆上分配**。Java 语义规定对象由 new 创建后属于托管对象,理论上位于堆上;HotSpot 默认在 TLAB(Thread Local Allocation Buffer)中通过指针碰撞快速分配。
2. **栈上分配 = 逃逸分析 + 标量替换**。HotSpot 中真正意义的"栈上分配"由逃逸分析驱动:
- 不逃逸(Not Escaped):对象只在创建方法内使用 → JIT 可**标量替换**(把对象字段拆散成寄存器/栈变量),或**分配在栈上**(-XX:+AllocateInNewlyAllocatedSpace / 老版本 ScalarReplaceable)
- 线程逃逸:跨方法调用但不出线程 → TLAB 快速路径
- 堆逃逸:对象引用被保存到全局变量/返回给其他线程 → 必须堆分配
3. **堆外内存 ≠ 自动迁移**。JVM 不会把堆上对象"搬"到堆外;堆外内存(DirectByteBuffer、Unsafe、MappedByteBuffer)是独立于 Java 堆的原生内存,由开发者主动使用,GC 只负责回收引用包装对象(Java 9+ 使用 Cleaner 机制保证释放)。
4. **栈上分配的收益与代价**:收益是无 GC 压力、访问快(栈内存与寄存器同速);代价是方法栈深度受限,且只有 JIT 后代码才可能优化(解释执行阶段一定走堆)。
**考察方向**:
- **深度**:能否说清"栈上分配"的真实机制是标量替换,而非把对象字面放到栈帧里
- **广度**:JIT、逃逸分析、TLAB、锁消除、锁粗化这一串 JIT 优化族群的关系
- **应用**:如何判断线上代码是否被标量替换(JFR / -XX:+TraceClassLoading / perf map),如何调优让热点方法触发逃逸分析
**延伸递进面试题**:
1. **逃逸分析在哪些情况下会失效?**
- 解答要点:对象被同步(synchronized this)、被反射访问、被赋值给 volatile、被序列化、被保存到静态字段、通过接口调用无法确定最终类型时,JIT 保守放弃标量替换
2. **JDK 9 引入的隐式 null 处理如何配合逃逸分析?**
- 解答要点:JDK 9+ 允许 null 作为字面量赋值给基本类型(implicit null),配合 Value-Based Classes 让 JIT 更容易判定不逃逸路径
3. **堆外内存(DirectByteBuffer)如何避免泄漏?**
- 解答要点:JDK 9+ 用 Cleaner 自动回收;JDK 8 依赖 PhantomReference + 手动 clean;监控用 -XX:MaxDirectMemorySize、jcmd VM.native_memory、JFR direct buffer event
4. **如何在线上用 JFR 定位标量替换机会?**
- 解答要点:启用 jdk.JFR 中的 compiler.* 事件 + jdk.AllocationInNewlyAllocatedObject 分析 TLAB 分配热点;jdisasm 看字节码是否被 inlined 并消除对象分配
面试官您好,这个问题的核心答案是**不一定,但绝大多数情况下在堆上**。默认情况下,new 出来的对象由 HotSpot 在堆的 TLAB 里通过指针碰撞快速分配,GC 负责回收。
但有两个特殊情况值得展开。
**第一,栈上分配**。这不是字面意义把对象字面放到栈帧,而是 JIT 编译器通过**逃逸分析**发现对象不出方法、不出线程,就把对象的字段拆开做**标量替换**——让局部变量代替对象,栈内存直接持有,GC 完全不用管。所以本质上,HotSpot 更常见的做法是"消除对象"而不是"把对象搬到栈"。逃逸分析有三种结果:不逃逸可标量替换,线程逃逸走 TLAB,堆逃逸只能堆分配。JDK 9 之后逃逸分析默认开启。
**第二,堆外内存**。这里有个常见误解——JVM 不会自动把堆上对象迁移到堆外,堆外内存是独立于 Java 堆的原生内存,需要开发者主动使用,比如 NIO 的 DirectByteBuffer、Netty 的 PooledByteBufAllocator、Unsafe.allocateMemory。这部分内存 GC 不管,需要显式释放或用 Cleaner 兜底。堆外内存的收益是避免 JVM 堆 GC 对大对象的开销,代价是排查问题更难,OOM 时看不到堆 dump。
所以准确的说法是:JVM 语义上对象属于托管对象默认在堆上;但在逃逸分析+JIT 优化下,可以不产生堆分配(标量替换);堆外内存是另一套独立的机制,不是"从堆搬过去"。
---
**核心考点定位**:考察 Java 内存引用体系的完整性理解,以及各引用类型在真实项目中的落地场景。
**知识点拆解**:
1. **强引用(Strong Reference)**
- 最常见的 `Object obj = new Object()`
- 只要强引用存在,对象永不被回收,即使内存不足会抛 OOM
- 打破强引用:显式置 null、方法局部变量出栈、强转失效
2. **软引用(Soft Reference)**
- `SoftReference<T>`,用于**内存敏感的缓存**
- 内存充足时保留,OOM 前会被回收
- 可用 `new SoftReference<>(obj, maxCapacity)` 精细控制容量(JDK 6+)
- 典型场景:图片缓存、Session 缓存、大文件缓存
3. **弱引用(Weak Reference)**
- `WeakReference<T>`,只要发生 GC 就回收(不再等内存不足)
- **必须**用 `WeakHashMap` 或类似结构封装,否则引用本身可能被回收
- 典型场景:**ThreadLocal**(Key 用 WeakReference,避免线程池线程不死导致 ThreadLocalMap 泄漏)、`System.identityHashCode()` 内部缓存、Guava Cache 的 weakKeys/weakValues
4. **虚引用(Phantom Reference)**
- `PhantomReference<T>`,**`get()` 永远返回 null**,不能通过它拿到引用对象
- 唯一用途:对象被 GC 时**收到通知**(入队到关联的 ReferenceQueue)
- 典型场景:**DirectByteBuffer** 堆外内存清理(JDK 8 用 Cleaner,JDK 9+ 底层就是虚引用 + Cleaner)、堆外大对象生命周期追踪、对象销毁回调
**考察方向**:
- **深度**:四种引用回收优先级、ReferenceQueue 机制、Cleaner 与虚引用的关系
- **广度**:与 GC 触发条件的联动(软引用受 -XX:SoftRefLRUPolicyMSPerMB 影响)、DirectByteBuffer 泄漏排查
- **应用**:LRU 缓存设计、ThreadLocal 内存泄漏根因、堆外内存监控
**延伸递进面试题**:
1. **ThreadLocal 为什么用弱引用?为什么还会内存泄漏?**
- 解答要点:Key 用 WeakReference 让 Key 可回收;但 Value 仍是强引用,若线程池线程不死,Key 被回收后变成"幽灵键",Value 依然占用内存;解决靠手动 `remove()`
2. **软引用如何做精细的 LRU 缓存?**
- 解答要点:设置 SoftReference 的 maxCapacity,配合 ReferenceQueue 主动清理已回收的引用;生产更推荐 Caffeine/Guava Cache(软引用+弱引用+时间过期组合)
3. **DirectByteBuffer 泄漏如何排查?**
- 解答要点:JDK 8 用 PhantomReference + ReferenceQueue;JDK 9+ 用 Cleaner;监控用 `jcmd VM.native_memory`、JFR `jdk.DirectByteBuffer`、`-XX:MaxDirectMemorySize` 硬上限
4. **弱引用能避免内存泄漏吗?**
- 解答要点:单独看不能(值仍是强引用),配合 WeakHashMap 可以,但注意 WeakHashMap 清理依赖 GC 触发时机,不能保证立即释放
面试官您好,Java 有四种引用强度,回收优先级从低到高是**虚、弱、软、强**,我用"什么时候能被 GC 回收"来区分。
**强引用**是最常见的,比如 `Object o = new Object()`,只要强引用存在,对象永不回收,内存不足直接 OOM。
**软引用** `SoftReference<T>`,用于内存敏感型缓存。内存充足时保留,OOM 前会被回收,可通过构造参数设置 maxCapacity 精细控制。典型场景是图片缓存、Session 缓存。生产环境一般用 Caffeine/Guava Cache,底层就是软引用 + ReferenceQueue。
**弱引用** `WeakReference<T>`,只要发生 GC 就回收,不等内存不足。**典型场景就是 ThreadLocal**——它的内部 ThreadLocalMap 用弱引用做 Key,目的是线程池线程不死时让 Key 可回收避免泄漏。但这里有个坑:Value 仍是强引用,所以手动 `remove()` 才是根本解法。另外 WeakHashMap、`System.identityHashCode()` 内部缓存也是弱引用。
**虚引用** `PhantomReference<T>`,`get()` 永远返回 null,唯一用途是**对象被 GC 时收到通知**,必须搭配 ReferenceQueue 使用。典型场景是 DirectByteBuffer 堆外内存清理,JDK 9+ 底层改用 Cleaner 机制,本质还是虚引用 + ReferenceQueue。所以虚引用不是"用来引用对象"的,而是"监听对象死亡"的钩子。
---
**核心考点定位**:考察 GC 存活判断的两种主流算法 + GC Roots 完整清单 + 分代假设的工程含义。
**知识点拆解**:
1. **两种判断算法**
- **引用计数法**:每个对象维护计数器,被引用+1、解除-1,为 0 即死。
- 优点:实现简单,回收即时
- 致命缺陷:**循环引用无法回收**(A 引用 B、B 引用 A,即使无外部引用,两者计数都非 0)
- 使用:Go 早期、Objective-C(ARC 后放弃)、C++ SmartPointer 的循环
- Java/Python **都不用**
- **可达性分析(Reachable Analysis)**:从一组 GC Roots 出发遍历引用链,能到达的存活,到不了的回收。
- Java、Python、JavaScript V8 都用
2. **GC Roots 完整清单**(HotSpot 实现)
- **栈引用**:所有活跃线程的栈帧局部变量、方法参数、运算栈
- **类静态字段**:类的 static 变量(含 static final 引用)
- **常量池引用**:运行时常量池里的类引用
- **被 synchronized 持有的对象**(监视器锁对象)
- **JNI 引用**:本地方法持有的 Java 对象
- **JVM 内部引用**:JIT 代码缓存、Unreachable class 元数据、GC 内部标记
3. **对象"不可达"不等于"已死"**
- 若重写了 `finalize()` 且未调用过,GC 会放到 F-Queue,"自救线程"执行 finalize
- 在 finalize 中重新赋值给变量则可回到可达(只有一次机会)
- JDK 9+ 弃用 finalize(性能差、不可预测),改用 **Cleaner**(虚引用机制,见 Q2)
4. **分代假设**
- 弱分代假说(Weak Generational Hypothesis):几乎所有对象朝生夕死
- 强分代假说(Strong Generational Hypothesis):熬过几次 GC 的对象更难死亡
- 是 JVM 分代设计的理论基础,也是"对象存活判断"和"GC 策略选择"的桥梁
**考察方向**:
- **深度**:为什么 Java 放弃引用计数、GC Roots 的完整清单、finalize vs Cleaner
- **广度**:可达性分析与三色标记、写屏障的关系;跨语言 GC 差异(Go trace vs Java tracing)
- **应用**:内存泄漏排查(MAT/VisualVM 看 GC Roots 短路径)、静态字段持有大对象、监听器未反注册
**延伸递进面试题**:
1. **引用计数法在什么语言中还在用?为什么?**
- 解答要点:Objective-C ARC(ARC 阶段引用计数,Swift 的 weak 打破循环);Go 早期用后废弃;核心原因是循环引用缺陷,主流 GC 语言都放弃了
2. **finalize() 有什么坑?为什么被弃用?**
- 解答要点:执行时机不确定、性能差(每次 GC 前要跑)、可能"复活"对象;JDK 9 标记 deprecated,改用 Cleaner
3. **如何用 MAT 排查内存泄漏?**
- 解答要点:Dump 堆 → MAT 打开 → 看 Dominator Tree 找大对象 → 看 GC Roots 路径(Shortest Path to GC Roots with path)→ 找出"不该被引用"的强引用
4. **弱分代假设对 GC 策略有什么影响?**
- 解答要点:新生代小、GC 频繁但快(复制算法);老年代大、GC 少而慢(标记-整理);这就是"为什么分代"的答案
面试官您好,JVM 判断对象存活主要用**可达性分析**,不是引用计数。
先说为什么不用引用计数:每个对象维护一个引用计数,被引用 +1,解除引用 -1,为 0 就回收。但**循环引用会死锁**——A 引用 B、B 引用 A,即使外部都不引用了,两者计数都非 0,永远回收不了。所以 Java、Python、JavaScript 都放弃了引用计数,只用可达性分析。Objective-C ARC 阶段用过引用计数,但也是通过 weak 属性来打破循环。
可达性分析的核心是从一组 **GC Roots** 出发遍历引用链,能到达的对象存活,到不了的回收。**GC Roots 包括**:
1. **栈引用**:所有活跃线程栈帧的局部变量、方法参数、运算栈
2. **静态字段**:类的 static 变量
3. **常量池引用**:运行时常量池里的类引用
4. **synchronized 持有的对象**(监视器锁对象)
5. **JNI 引用**:本地方法持有的 Java 对象
6. **类加载器** + JVM 内部引用(GC 实现细节)
这里有个补充点:对象"不可达"不等于"已死"。如果重写了 `finalize()` 且还没调用过,GC 会把它放到 F-Queue,让"自救线程"执行 finalize;在 finalize 里重新赋值给变量就能回到可达,但只有一次机会。JDK 9 之后 finalize 被弃用,改用 **Cleaner** 机制(基于虚引用 + ReferenceQueue),更可控也更快。
最后要提到**分代假设**——这是从"存活判断"过渡到"GC 策略选择"的桥梁:**几乎所有对象朝生夕死,熬过几次 GC 的对象更难死**。所以新生代小、GC 频繁但快(复制算法),老年代大、GC 少而慢(标记-整理)。这就是为什么 JVM 要分代。
---
**核心考点定位**:考察三大经典 GC 算法的机制与权衡,以及现实 GC 器如何组合演化。
**知识点拆解**:
1. **标记-清除(Mark-Sweep)**
- 流程:从 GC Roots 标记存活 → 统一清理未标记
- 优点:实现简单、无需移动对象
- 缺点:**内存碎片**,长时间运行后大对象无法分配
- 使用者:CMS 老年代、ZGC 早期阶段
2. **复制(Copy)**
- 流程:堆分两半(Eden + Survivor),GC 时把存活复制到另一半,清空旧半
- 优点:**无碎片**、存活少时效率极高
- 缺点:**浪费空间**(存活多时开销大),所以只适合新生代
- HotSpot 优化:Eden:From:To = **8:1:1**,Survivor 从 512MB 降到 16MB,让老年代吃满剩余空间
- 使用者:所有分代 GC 的新生代(Serial、ParNew、Parallel、G1、ZGC)
3. **标记-整理(Mark-Compact)**
- 流程:标记存活 → **把存活对象压缩到一侧**,避免碎片
- 优点:无碎片、空间利用率高
- 缺点:**移动对象要处理所有引用**(写屏障 / TLAB 重建),STW 时间随老年代增长
- 使用者:ParallelGC、G1(Region 间)、ZGC、Shenandoah
4. **GC 器 = 算法组合 + 执行方式**
| GC 器 | 新生代 | 老年代 | 停顿特征 |
|------|-------|--------|---------|
| Serial | 复制 | 标记-整理 | 单线程,短停顿 |
| ParallelGC(默认 JDK 8) | 复制 | 并行标记-整理 | 高吞吐,停顿长 |
| CMS | ParNew(复制) | 并发标记-清除 | 低延迟但有碎片 |
| G1(JDK 9+ 默认) | Region 复制 | Region 间标记-整理 | 可预测停顿 |
| ZGC / Shenandoah | 同堆 | 并发标记-整理 + 读屏障 | <10ms 停顿 |
5. **核心权衡三角**
- **吞吐 vs 停顿 vs 空间**——三者不可兼得
- 新生代选复制(存活少)= 用空间换吞吐
- 老年代选标记-整理(无碎片)= 用停顿换空间
- 并发 GC 用写屏障/读屏障 = 用复杂度换停顿
**考察方向**:
- **深度**:写屏障机制(Card Table、Remembered Set)、整理对象移动的成本
- **广度**:各 GC 器的算法组合、ZGC/Shenandoah 的读屏障演进
- **应用**:JDK 9+ 为什么 G1 成默认、什么时候换 ZGC、Eden 比例调优
**延伸递进面试题**:
1. **为什么新生代用复制不用标记-清除?**
- 解答要点:朝生夕死,存活少,复制快无碎片;标记-清除新生代虽快但碎片累积影响 Eden 分配
2. **Eden:Survivor = 8:1 是什么原理?**
- 解答要点:JDK 8 默认 8:1:1,Survivor 只留约 10% 空间;存活多时(超 50%)直接晋升老年代;可通过 `-XX:PretenureSizeThreshold` 和 `-XX:MaxTenuringThreshold` 调优
3. **写屏障是什么?为什么并发 GC 必需?**
- 解答要点:写屏障是对象引用赋值时插入的额外代码(比如 CardTable 记录跨代指针),让 GC 能高效处理并发修改;ZGC 用读屏障(Load Barrier + 颜色指针)进一步减少 STW
4. **ZGC 如何实现 <10ms 停顿?**
- 解答要点:并发标记+并发整理+读屏障+多映射(同一虚拟地址可映射多个物理地址,移动对象时改映射而非对象地址)
面试官您好,JVM 的经典 GC 算法有三种:**标记-清除、复制、标记-整理**,然后现实 GC 器都是这三种算法在不同代上的组合。
**标记-清除**:从 GC Roots 遍历标记存活,再统一清理未标记。实现简单,但有**内存碎片**问题,长时间运行后大对象可能分配不到连续空间。
**复制**:堆分两半,GC 时把存活复制到另一半,清空旧半。优点是**无碎片**、存活少时极快,缺点是浪费一半空间,所以只适合**新生代**。HotSpot 优化成 **Eden:From:To = 8:1:1**——Eden 占 80%,两个 Survivor 各 10%,老年代吃满剩余空间;如果 Survivor 装不下就把对象提前晋升。
**标记-整理**:标记存活后,把存活对象**压缩到一侧**避免碎片。无碎片但**要移动对象**,移动时要处理所有引用(写屏障),STW 时间随老年代增长。所以适合老年代。
这三种算法怎么组合成了不同的 GC 器?
- **ParallelGC**(JDK 8 默认)= 新生代复制 + 老年代并行标记-整理,追求高吞吐
- **CMS** = ParNew 复制 + 并发标记-清除,追求低延迟但有碎片,JDK 14 弃用
- **G1**(JDK 9+ 默认)= **Region 复制 + Region 间标记-整理**,把整个堆切成等大的 Region,回收可以选最脏的 Region 优先,从而**可预测停顿时间**(`-XX:MaxGCPauseMillis`)
- **ZGC / Shenandoah** = 并发标记-整理 + **读屏障** + 多映射,实现**<10ms 停顿**,堆可以到 TB 级
总结一句话:**新生代选复制(存活少,无碎片),老年代选标记-整理(无碎片,用停顿换空间)**。这个选择的基础就是**弱分代假说**——几乎所有对象朝生夕死。而并发 GC 想进一步缩短停顿,就必须引入写屏障、读屏障、颜色指针这些机制,本质还是这三种算法的演变。
---
**核心考点定位**:考察分代机制的结构性理解(Eden/Survivor/Old 具体划分)+ 分代设计的动机(弱分代假说)+ 晋升机制细节。
**知识点拆解**:
1. **堆的完整结构(HotSpot 默认)**
```
Heap
├── Young Generation (约 1/3)
│ ├── Eden (约 80%) # 新对象首次分配在这里
│ ├── From Survivor (约 10%) # 上一次存活对象
│ └── To Survivor (约 10%) # 本次存活对象目标
└── Old Generation (约 2/3) # 长寿对象 + 大对象
```
- **Eden:From:To = 8:1:1** 是 HotSpot 优化:Survivor 只保留约 10%,让老年代吃满剩余空间
- JDK 8 之后:MetaSpace 独立于堆,永久代→元空间
2. **为什么分代?——弱分代假说**
- **弱分代假说**:几乎所有对象朝生夕死
- **强分代假说**:熬过几次 GC 的对象更难死
- **推论**:新生代存活少 → 用**复制算法**(无碎片、快);老年代存活多 → 用**标记-整理**(无碎片、空间利用率高)
- **反证**:如果堆不分代,全堆统一用标记-清除,会有碎片;用复制算法,空间浪费且大对象无法处理;用标记-整理,每次 FullGC 都要移动大部分对象,停顿爆炸
- 所以分代本质是**用空间的复杂性,换取算法最优化的可能性**
3. **对象晋升的 4 条规则**(HotSpot 实际实现)
- **年龄晋升**:对象每经历一次 Young GC 年龄 +1,超过 `-XX:MaxTenuringThreshold`(默认 15,动态计算)就晋升
- **动态年龄判定**:Survivor 中同龄对象总和 > Survivor 50%,该年龄及更大的全部晋升(避免"熬到老")
- **大对象晋升**:超过 `-XX:PretenureSizeThreshold` 直接在老年代分配(大对象在 Eden 复制开销大)
- **空间分配担保**:Survivor 装不下就把剩余对象搬到老年代
4. **两类 GC 的边界**
- **Young GC**:只回收新生代,Survivor 之间来回复制
- **Mixed GC**(G1 特有):Young GC + 部分老年代 Region(按"脏度"排序)
- **Full GC**:整代回收(新生代 + 老年代 + Metaspace),STW 长,是最后手段
- 老年代触发:老年代空间不足、Survivor 晋升失败、Metaspace 不足、System.gc() 显式调用
5. **分代的代价:跨代引用**
- 老年代对象引用新生代对象,FullGC 时若从老年代根遍历新生代会 O(n)
- **Remembered Set**:每个老年代 Region 维护一个跨到年轻代的引用集合(G1)
- **Card Table**:每 512 字节标记一个 Card,写屏障时更新 Card 状态(CMS/Parallel)
- 写屏障(Write Barrier):对象引用赋值时插入的检查代码,让 GC 高效处理并发修改
**考察方向**:
- **深度**:4 种晋升条件、写屏障/Remembered Set 机制、动态年龄判定
- **广度**:不同 GC 器(G1/ZGC/Shenandoah)是否分代、"不分代"的意义
- **应用**:`-Xmn` / `-XX:SurvivorRatio` / `-XX:MaxTenuringThreshold` 调优;晋升过早导致老年代撑爆
**延伸递进面试题**:
1. **为什么 Survivor 只有 10% 空间?**
- 解答要点:新生代存活率通常 < 10%,留 20%(From+To)已足够;剩余 80% 给 Eden 保证分配效率;Survivor 太小也能逼早熟对象晋升,避免"熬到老"
2. **MaxTenuringThreshold 是动态的吗?**
- 解答要点:是,HotSpot 会动态计算"能让 Survivor 里存活对象总年龄刚好填满 Survivor 空间"的阈值;也可 `-XX:MaxTenuringThreshold=15` 硬设
3. **G1 的 Region 和传统分代有什么区别?**
- 解答要点:G1 是**逻辑分代**(Region 可标记为 Young/Old/Humongous),不是物理分代;回收可以选任意 Region,通过 Remembered Set 处理跨代引用,避免全堆扫描
4. **跨代引用怎么处理?**
- 解答要点:Remembered Set(G1)或 Card Table(CMS/Parallel),配合写屏障在对象引用赋值时更新;避免 Full GC 时全堆遍历
5. **JDK 21+ 的分代 ZGC 是什么变化?**
- 解答要点:JDK 21 之前 ZGC 是"不分代"的(全堆统一),JDK 21 引入分代 ZGC(Young/Old 分区),进一步降低停顿、提高吞吐
面试官您好,Java 堆的分代核心是**为了不同对象用不同算法**,这是 GC 设计的核心权衡。
先说**为什么分代**,核心是**弱分代假说**——几乎所有对象朝生夕死,熬过几次 GC 的对象更难死。如果堆不分代:用复制算法浪费空间且大对象无法处理,用标记-整理每次 FullGC 都要移动大部分对象停顿爆炸。所以分代本质是**用空间复杂性换算法最优化的可能性**。
再说**如何分代**,HotSpot 堆分为新生代和老年代两部分。
**新生代占约 1/3**,内部又分 Eden、From Survivor、To Survivor 三区,比例是 **Eden:From:To = 8:1:1**。新对象默认先在 Eden 分配,Eden 满触发 Young GC,存活对象复制到 To Survivor,Survivor 之间来回交替(Eden:80%、两个 Survivor 各 10%——Survivor 只留 10% 是为了逼早熟对象晋升,避免"熬到老")。
**老年代占约 2/3**,存长寿对象和大对象。JDK 8 之后还有独立的 Metaspace(不在堆上)。
**对象晋升老年代的 4 条规则**:
1. **年龄晋升**:GC 次数达 `MaxTenuringThreshold`(默认 15,动态计算)
2. **动态年龄判定**:Survivor 中同龄对象总和 > 50%,该年龄及更大全部晋升
3. **大对象晋升**:超 `PretenureSizeThreshold` 直接在老年代分配,避免 Eden 复制开销
4. **空间分配担保**:Survivor 装不下就搬到老年代
**两类 GC 的边界**:**Young GC** 只回收新生代;**Mixed GC**(G1 特有)是 Young GC + 部分老年代 Region(按"脏度"排序);**Full GC** 是整代回收,是最后手段。所以说"老年代只有 Full GC 才动"是不准确的——G1 的 Mixed GC 也会回收部分老年代 Region。
**分代的代价是跨代引用问题**:老年代对象引用新生代对象时,Full GC 若全堆遍历会 O(n) 爆炸。所以引入了 **Remembered Set**(G1)或 **Card Table**(CMS/Parallel)+ **写屏障**,让 GC 只处理"脏"的引用,避免全堆扫描。
最后补充一点演进:**ZGC 早期是不分代的**(全堆统一),JDK 21 引入**分代 ZGC**,进一步降低停顿、提高吞吐。
---
**核心考点定位**:考察 STW 的本质动机(状态一致性)+ 实现机制(Safepoint)+ 现代 GC 的演化方向。
**知识点拆解**:
1. **STW 的本质:状态一致性的锁**
- GC 判断存活需要**引用图**,而线程不断修改引用图,如果不暂停就会**竞态**(引用指向刚被回收的对象、对象在标记中途被创建/销毁)
- 所以 STW = **"GC 和线程之间的互斥锁"**,用短暂停顿换取分析的正确性
- 类比:**拍合照时所有人都静止**——不是"暂停"本身重要,而是"冻结瞬间状态"才重要
2. **STW 的实现:Safepoint 机制**
- JVM 不会立即暂停所有线程,而是通知线程跑到最近的 **Safepoint**(安全点)
- Safepoint 是字节码中的特定位置:方法调用、循环回跳、异常跳转、分支跳转
- 大循环里没有 safepoint 会拖长 STW(HotSpot 有 `-XX:LoopSafepointPollSize` 让长循环周期性检查)
- 排查:`jcmd <pid> VM.print_safepoints` 或 JFR `jdk.Safepoint` 事件
3. **GC 中的 STW 阶段**
- **Initial Mark(初始标记)**:只标 GC Roots,快,必须 STW
- **Concurrent Mark(并发标记)**:GC 线程和用户线程并发标记,不 STW
- **Remark(重新标记)**:处理并发标记期间"引用变动"的部分,靠写屏障/Remembered Set 缩短,STW
- **Concurrent Sweep(并发清除)**:并发清理,不 STW(CMS 有,G1/ZGC 用 Concurrent Relocate)
- **Remark 是 STW 优化的重点**:G1 通过缩小 Region 减少 Remark 范围
4. **CMS 的退化:Concurrent Mode Failure**
- 并发标记-清除期间老年代空间不足 → 触发 Full GC
- Full GC 是**串行 STW**,停顿从几十 ms 飙到秒级
- 这就是用户说的"退化回单线程",术语叫 **Concurrent Mode Failure**
- 解决:调大老年代、换 G1(G1 可预测停顿避免退化)
5. **STW 的极限:ZGC / Shenandoah**
- **ZGC**(JDK 15 GA):STW <1ms,堆可 16TB
- **技术**:读屏障(Load Barrier)+ 多映射(Multi-Mapping)——对象移动时改虚拟地址映射而非对象引用
- **Shenandoah**:写屏障+ Brooks 转发(对象头存转发指针),STW <10ms
- 但**STW 并未消除**:Full GC 触发(Metaspace 满、System.gc())仍是秒级
6. **STW 观测**
- `-Xlog:gc*` 或 `-XX:+PrintGCDetails -XX:+PrintGCTimeStamps`
- JFR 事件:`jdk.GCPhasePause`、`jdk.GCHeapSummary`
- Arthas `dashboard`、Prometheus + JVMExporter
- 生产告警:单次 STW > 200ms 或频繁 FullGC 都应告警
**考察方向**:
- **深度**:Safepoint 机制、Remark 的缩短技巧、ZGC 的多映射原理
- **广度**:各 GC 器的 STW 分布(CMS vs G1 vs ZGC)、Full GC 与其他 GC 的对比
- **应用**:线上 GC 问题排查(长停顿、频繁 FullGC)、`-XX:MaxGCPauseMillis` 调优
**延伸递进面试题**:
1. **Safepoint 是什么?为什么长方法会拖长 STW?**
- 解答要点:Safepoint 是字节码中线程可暂停的位置;长循环无 safepoint 会阻塞其他线程;HotSpot 通过 `-XX:LoopSafepointPollSize` 自动插入 safepoint 检查
2. **CMS 为什么被弃用?G1/ZGC 有什么优势?**
- 解答要点:CMS 有碎片、Concurrent Mode Failure、JDK 14 弃用;G1 可预测停顿、Region 结构避免碎片;ZGC <10ms 停顿、支持 TB 级堆
3. **如何在线上定位长 STW 停顿?**
- 解答要点:GC 日志(`-Xlog:gc*`)、JFR `jdk.GCPhasePause` 事件、Arthas `dashboard`、Prometheus JVMExporter;关注 `Pause Remark` 和 `Full GC` 频率
4. **Full GC 一定 STW 吗?**
- 解答要点:传统 Full GC 一定 STW(几分钟到几秒),ZGC 无 Full GC 概念;G1 的 Full GC 也是 STW,但通过 Mixed GC 尽量避免
5. **ZGC 的"多映射"是什么原理?**
- 解答要点:同一虚拟地址映射到多个物理地址,移动对象时改映射而非对象引用;配合读屏障在读取时检查指针有效性;避免了移动对象时同步引用
面试官您好,**Stop The World 的核心动机是"状态一致性"**——GC 判断对象存活需要引用图,但线程不断修改引用图,如果不暂停就会竞态(引用指向刚被回收的对象、对象在标记中途被创建或销毁)。所以 STW 本质是**"GC 和线程之间的互斥锁"**,用短暂停顿换分析的正确性。
**实现机制是 Safepoint**。JVM 通知线程跑到最近的安全点才暂停,安全点是字节码中的特定位置——方法调用、循环回跳、异常跳转。有个坑:长方法如果没有安全点,其他线程会一直等它跑完 safepoint,STW 被拖长。HotSpot 通过 `-XX:LoopSafepointPollSize` 自动在长循环插入 safepoint 检查。
**GC 中具体哪些阶段 STW**?
- **Initial Mark(初始标记)**:只标 GC Roots,必须 STW,但很快
- **Concurrent Mark(并发标记)**:GC 线程和用户线程并发,不 STW
- **Remark(重新标记)**:处理并发标记期间的变动,靠写屏障/Remembered Set 缩短,STW
- **Concurrent Sweep(并发清除)**:并发清理,不 STW(CMS 有,G1/ZGC 用 Concurrent Relocate)
所以**STW 主要集中在初始标记和重新标记两个阶段**,G1 通过缩小 Region 让 Remark 范围更小。
**并发 GC 会退化吗?**——会的,这就是 **Concurrent Mode Failure**。比如 CMS 并发标记期间老年代空间不足,就退化为 **Full GC**,停顿从几十 ms 飙到秒级,这就是常说的"退化回单线程",是线上 GC 事故的常见根因。G1 通过可预测停顿(`-XX:MaxGCPauseMillis`)尽量避免退化。
**STW 能完全消除吗?**——目前不能完全消除。ZGC 通过**读屏障+多映射**(同一虚拟地址映射多个物理地址,移动对象时改映射而非改对象引用)把 STW 压到 <1ms;Shenandoah 用**写屏障+Brooks 转发**做到 <10ms。但 Full GC 触发(Metaspace 满、`System.gc()`)仍是秒级,STW 更像**"暂时性技术约束"**而非"绝对不可能"。
**线上怎么观测**?`-Xlog:gc*` 打 GC 日志、JFR `jdk.GCPhasePause` 事件、Arthas `dashboard`、Prometheus + JVMExporter。生产上单次 STW > 200ms 或频繁 FullGC 都应该告警。
所以 STW 的哲学是:**它不是可以回避的问题,而是可以用并发技术不断缩短的问题**。从 Serial 的秒级,到 CMS 的百 ms 级,到 G1 的几十 ms,到 ZGC 的 <10ms,每一步都是并发技术对 STW 的极限压缩,但都不是消除。
---
**核心考点定位**:考察 GC 流程的**双层理解**——对象晋升路径 + 算法执行阶段,以及并发 GC 的关键环节。
**知识点拆解**:
1. **GC 流程的两层含义**
- **对象生命周期流程**:Eden → Survivor 交替 → 老年代 → 回收
- **算法执行流程**:Initial Mark → Concurrent Mark → Remark → Concurrent Sweep
- 面试答"完整流程"要**两层都说**,只说一层扣分
2. **Young GC(Minor GC)完整流程**
- 触发:Eden 满
- 动作:**STW**
1. 清空 Eden(不标不整理,直接归零)
2. 将 Eden 存活对象复制到 To Survivor(**复制算法**)
3. 将 From Survivor 存活对象按年龄判断:
- 未达阈值 → 复制到 To Survivor,年龄 +1
- 达到 `MaxTenuringThreshold`(默认 15)→ 晋升老年代
4. Survivor 装不下 → **空间分配担保**:能塞老年代就塞,塞不下晋升失败
5. **交换 From 和 To 指针**
6. 清空 From Survivor
- 特点:STW 短(几十 ms),频率高(每秒数次)
3. **Full GC 完整流程(CMS 为例,G1 有 Mixed GC)**
- 触发:老年代满、Metaspace 满、`System.gc()`、Survivor 晋升失败
- CMS 四阶段:
1. **Initial Mark(初始标记)**:STW,标 GC Roots,很快
2. **Concurrent Mark(并发标记)**:GC 线程 + 用户线程并发遍历,不 STW
3. **Remark(重新标记)**:STW,处理并发标记期间的引用变动(靠**写屏障** / **Card Table** 缩短)
4. **Concurrent Sweep(并发清除)**:并发清理死对象,不 STW(有碎片)
- Full GC 传统流程(非并发):**STW** 整代标记 → **标记-整理**(存活压缩到一侧)→ 清理尾部
4. **Mixed GC(G1 特有)**
- Young GC + 部分老年代 Region(按"脏度"排序优先回收)
- 通过 `-XX:MaxGCPauseMillis` 目标停顿预测要回收哪些 Region
- 避免 Full GC(G1 尽量避免退化到 Full GC)
5. **关键机制:写屏障 + Remembered Set**
- 老年代引用新生代对象时,写屏障在赋值时更新 Card(512 字节一片)
- GC 时只处理"脏" Card,避免全堆扫描
- G1 用 Remembered Set 记录跨 Region 引用
6. **观测与调优**
- `-Xlog:gc*`(JDK 9+):完整 GC 日志,含 STW 时长
- `-Xlog:gc+heap`:堆各区变化
- JFR:`jdk.GCPhasePause`、`jdk.GCHeapSummary`、`jdk.GCObjectAllocationInNewTLAB`
- Arthas `dashboard`、Prometheus + JVMExporter
- 生产告警:单次 STW > 200ms、频繁 FullGC、Metaspace 增长
**考察方向**:
- **深度**:写屏障/Remembered Set/Eden:From:To 交换、Remark 如何缩短
- **广度**:不同 GC 器(CMS/G1/ZGC)的流程差异、ZGC 无 Full GC 的意义
- **应用**:GC 日志分析、老年代 OOM 排查、晋升失败的处理
**延伸递进面试题**:
1. **Young GC 具体动作是什么?为什么 Eden/From 直接清零?**
- 解答要点:Eden 里没存活的不复制,存活全复制到 To Survivor;Eden/From 已"清空",无需标记,直接归零最快
2. **Remark 为什么需要 STW?如何缩短?**
- 解答要点:Remark 要处理并发标记期间的引用变动,必须 STW 保证一致性;通过写屏障让变动可追踪,Remark 只处理"脏"引用(Card Table / Remembered Set)
3. **G1 如何避免 Full GC?**
- 解答要点:用 Mixed GC(Young + 部分老年代 Region)替代 Full GC;`-XX:MaxGCPauseMillis` 目标停顿;`-XX:InitiatingHeapOccupancyPercent` 阈值控制启动并发标记的时机
4. **如何在生产上观察 GC 流程?**
- 解答要点:`-Xlog:gc*` 完整日志、JFR 事件、Arthas dashboard、关注 `Pause Remark` 和 `Full GC` 频率
5. **ZGC 的 GC 流程和传统 GC 有什么本质不同?**
- 解答要点:无 Full GC 概念;并发标记+并发整理都用读屏障;多映射让对象移动时改映射而非改引用;STW 只有 Initial Mark 和 Final Relocate 两个瞬间
面试官您好,GC 的"完整流程"其实有两层含义,我按层说。
**第一层是对象晋升流程**。新对象先在 Eden 分配,Eden 满触发 **Young GC**:Eden 里的存活对象复制到 To Survivor,From Survivor 里的对象年龄 +1 后继续留 Survivor 或晋升老年代,然后交换 From 和 To 的指针,清空 Eden 和 From。反复几次 Young GC,对象年龄累积超过 `MaxTenuringThreshold`(默认 15)就晋升老年代。这一步的核心是**复制算法**,靠 Survivor 交替避免碎片。
**第二层是 GC 算法执行流程**,以 CMS 为例(G1 有 Mixed GC,ZGC 无 Full GC):
1. **Initial Mark(初始标记)**:STW,只标 GC Roots,非常快
2. **Concurrent Mark(并发标记)**:GC 线程和用户线程并发遍历引用图,不 STW
3. **Remark(重新标记)**:STW,处理并发标记期间的引用变动,靠**写屏障** + **Card Table / Remembered Set** 缩短范围
4. **Concurrent Sweep(并发清除)**:并发清理死对象,不 STW(有碎片问题)
如果是 **Full GC**(非并发),流程是 STW 整代标记 → **标记-整理**(存活压缩到一侧)→ 清理尾部。Full GC 触发条件:老年代满、Metaspace 满、`System.gc()`、Survivor 晋升失败。
**G1 的关键差异**:用 **Mixed GC**(Young + 部分老年代 Region)替代 Full GC,通过 `-XX:MaxGCPauseMillis` 目标停顿预测要回收哪些 Region,尽量避免退化到 Full GC。
**贯穿全程的机制是写屏障 + Remembered Set**:老年代引用新生代对象时,写屏障在赋值时更新 Card,GC 时只处理"脏"的引用,避免全堆扫描。
**生产上怎么观察?** 用 `-Xlog:gc*` 打完整 GC 日志看 STW 时长,JFR `jdk.GCPhasePause` 看各阶段耗时,关注 `Pause Remark` 和 Full GC 频率;生产上单次 STW > 200ms 或频繁 FullGC 都应告警。
一句话总结:**GC 流程 = 对象晋升(Eden → Survivor → Old)+ 算法执行(Initial Mark → Concurrent Mark → Remark → Sweep)**,前者决定"对象去哪",后者决定"GC 怎么算"。
---
**核心考点定位**:考察三色标记的**严格定义**+**并发场景的内存安全问题**+**修复机制(写屏障/读屏障)**。
**知识点拆解**:
1. **三色严格定义(HotSpot 采用)**
- **白色(White)**:**未被 GC 访问过**,可能是垃圾也可能是存活(初始状态)
- **灰色(Gray)**:已被访问,但**引用的对象还有未访问**(工作队列 / 待扫描)
- **黑色(Black)**:已访问,且**所有引用都已扫描完毕**(确定存活)
2. **算法流程**
```
1. 所有对象初始为白色
2. GC Roots 全部染黑
3. 取一个黑节点,扫描其引用:
- 引用的对象若为白 → 染灰、加入工作队列
- 该黑节点所有引用扫描完 → 完成
4. 从工作队列取灰节点:
- 扫描其引用,白→灰、入队
- 所有引用扫描完 → 染黑
5. 工作队列为空时结束
6. 剩余白色对象 = 垃圾,回收
```
**不变式(正确性条件)**:
- **规则 1**:黑不能指白(否则漏回收)
- **规则 2**:灰不能指黑(否则可能误回收,因为灰节点扫描时看不到已染黑的子节点)
3. **并发场景的两大问题(重点!)**
三色标记的核心动机是**并发标记**——让 GC 与用户线程并发扫描,减少 STW。但并发带来两个引用变动:
- **对象消失(漏回收)**:黑节点 X 原本引用白对象 Y,扫描后删链;紧接着用户线程创建黑节点 Z 引用 Y;此时 Y 是白,被误回收 → **Y 活着但被回收**
- **对象复活(误回收)**:黑节点 X 引用白对象 Y 时,用户线程先创建白节点 Z 引用 X,扫描完 Y 后删掉 X→Y 链;此时 Y 被误认为白垃圾,但通过 Z→X→? 关系其实……(更常见是**误回收**:黑节点被引用后仍被错认为垃圾)
**准确表述**:
- **黑指白(Broken by Black→White add)**:漏回收
- **白指黑(Broken by White→Black add)**:误回收
4. **两种修复方案**
| 修复方案 | 屏障类型 | 使用者 | 核心思想 |
|---------|---------|-------|---------|
| **写屏障** | Write Barrier | CMS、G1、ZGC(经典) | 修复"黑指白":**对象引用变白 → 父对象重染灰** |
| **读屏障** | Read/Load Barrier | Shenandoah、ZGC(现代) | 修复"白指黑":**读指针时检查有效性,无效则重新标记** |
5. **ZGC 的启示**
- **多映射(Multi-Mapping)**:同一虚拟地址映射多个物理地址,移动对象时改映射而非引用
- **读屏障**:读取指针时检查指针有效性(Validated Address)
- **STW 只有两瞬间**:Initial Mark 和 Final Relocate,都 <1ms
- 三色标记 + 读屏障 = ZGC 的核心技术组合
6. **CMS 的具体流程中的三色标记**
- Initial Mark(STW):把 GC Roots 染黑
- Concurrent Mark:三色并发扫描(灰→黑、白→灰)
- Remark(STW):处理并发期间的引用变动,靠写屏障让变动可追踪
**考察方向**:
- **深度**:三色定义准确表述、内存安全问题的严格推理、写屏障具体规则
- **广度**:写屏障 vs 读屏障的取舍、ZGC/Shenandoah 的现代方案
- **应用**:Remark 如何缩短、CMS/G1/ZGC 的三色标记实现差异
**延伸递进面试题**:
1. **三色标记的"对象消失"问题具体怎么走通?**
- 解答要点:黑节点 X 引用白节点 Y → 扫描 X 完成后断链 X→Y → 用户线程创建黑节点 Z 引用 Y → Y 变灰再变黑 → 结束遍历 Y 是黑(正常);但如果 Y 从未被访问,Y 保持白 → 被误回收
2. **写屏障具体怎么处理?**
- 解答要点:写屏障在对象引用赋值时插入的检查:如果新引用指向白对象,把父对象重染灰;HotSpot 中通过 `-XX:+CMSClassUnloadingEnabled` 相关逻辑实现
3. **为什么 ZGC 从写屏障转向读屏障?**
- 解答要点:写屏障频繁触发(每次赋值)影响吞吐;读屏障只在读取指针时检查,对代码侵入小;配合多映射实现"移动对象不改引用"
4. **Shenandoah 的 Brooks 转发是什么?**
- 解答要点:对象头存转发指针,读屏障检查指针有效性时顺带完成对象移动;STW <10ms
5. **三色标记和可达性分析的关系?**
- 解答要点:三色标记是可达性分析的**并发实现**,本质都是从 GC Roots 遍历;区别在是否并发、如何处理引用变动
面试官您好,**三色标记算法**是并发 GC 的核心算法,让 GC 扫描和用户线程并发运行,减少 STW。
**三色严格定义**(HotSpot 采用):
- **白色**:从未被 GC 访问过,可能是垃圾也可能是存活(**初始状态**)
- **灰色**:已被访问,但**引用的对象还有未访问的**(工作队列,待扫描)
- **黑色**:已访问,且**所有引用都扫描完毕**(确定存活)
**算法流程**:GC Roots 全部染黑 → 取灰节点扫描子节点,白节点变灰入队,灰节点扫描完变黑 → 工作队列为空时结束 → **剩余白色对象就是垃圾,回收**。
**核心思想**:白色 = 潜在垃圾,黑色 = 确定存活,灰色 = 中间态。
**并发带来的两个内存安全问题**:
1. **对象消失(漏回收)**:黑节点 X 引用白节点 Y → 扫描完成断链 X→Y → 用户线程创建黑节点 Z 引用 Y → 此时 Y 从未被访问保持白,被误回收。这违反"黑不能指白"规则。
2. **对象复活(误回收)**:白节点变黑后仍被断链,导致实际存活的对象被标成白。这违反"灰不能指黑"规则。
**两种修复方案**:
- **写屏障(Write Barrier)**:修复"黑指白"问题。规则是**当对象引用被赋值时,如果新引用指向白对象,把父对象重染灰**。使用者:CMS、G1、ZGC 经典模式。
- **读屏障(Read/Load Barrier)**:修复"白指黑"问题。规则是**读指针时检查有效性,无效则重新标记为黑**。使用者:Shenandoah、ZGC 现代模式。
**为什么 ZGC 从写屏障转向读屏障?** 写屏障每次赋值都触发,影响吞吐;读屏障只在读取指针时检查,对代码侵入小。ZGC 配合**多映射**(同一虚拟地址映射多物理地址,移动对象改映射而非改引用)实现 STW <1ms。
**CMS 中的三色标记**:Initial Mark(STW)染黑 GC Roots → Concurrent Mark 并发三色扫描 → Remark(STW)靠写屏障追踪并发期间的引用变动。
一句话总结:**三色标记是可达性分析的并发实现**——本质都是从 GC Roots 遍历,但通过三色状态管理并发下的引用变动,靠写屏障或读屏障保证内存安全。
---
**核心考点定位**:考察 OOM 的**分类完整性**+**每种类型的触发场景**+**监控和解决手段**。
**知识点拆解**:
1. **OOM 本质**
- Java 抛 `java.lang.OutOfMemoryError` 时,JVM **不会立即退出**(除非是特定类型或 GC 前最后一次尝试失败)
- OOM 是**运行时异常**(Error 而非 Exception),意味着"JVM 认为无法通过 GC 解决"
2. **OOM 完整分类(HotSpot 常见 8 种)**
| 类型 | 消息 | 触发场景 |
|------|------|---------|
| Java heap space | 堆溢出 | 缓存无界、集合未清、监听器未反注册 |
| GC overhead limit exceeded | GC >98% 但回收 <2% | 内存接近满但 GC 无法有效回收 |
| Metaspace | 元空间溢出(JDK 8+) | 动态代理、CGLIB、Groovy 脚本、ClassLoader 泄漏 |
| Direct buffer memory | 直接内存溢出 | NIO/Netty 未释放 DirectByteBuffer |
| unable to create new native thread | 线程过多 | Thread 未复用、线程池无上限 |
| Requested array size exceeds VM limit | 数组过大 | 一次申请超大数组 |
| Compressed class space | 压缩类空间满 | `-XX:CompressedClassSpaceSize` 满 |
| unable to make continuation | Loom 协程溢出 | Java 21+ 虚拟线程相关 |
3. **每种类型的典型根因**
- **堆溢出**:
- 静态集合持续增长(`static Map`、`static List` 无清理)
- 缓存无 TTL、无淘汰策略
- 大对象一次性加载(`readAllBytes`、`readLines`)
- 监听器未反注册导致回调链泄漏
- ThreadLocal 未 remove 导致线程膨胀
- **Metaspace 溢出**:
- CGLIB/ByteBuddy 动态生成类
- Groovy/JavaScript 脚本引擎反复编译
- 热部署反复创建 ClassLoader
- 反射生成大量代理类
- **DirectBuffer 溢出**:
- Netty `ByteBuf` 未 `release()`
- `FileChannel.map()` 未 `unmap`
- NIO `ByteBuffer.allocateDirect` 未 GC 回收(JDK 8 需 PhantomReference 手动清理)
- **GC overhead limit exceeded**:
- 堆使用率 >98% 且 GC 后仍 >98%
- 常见于内存泄漏后期
- **线程过多**:
- `Thread.start()` 无上限
- 线程池队列无界或拒绝策略不当
- ThreadLocal 泄漏导致线程池线程持有大对象
4. **监控手段**
- **JVM 启动参数**:
- `-XX:+HeapDumpOnOutOfMemoryError`:OOM 时自动 dump(救命参数!)
- `-XX:HeapDumpPath=/var/log/dumps/`:dump 路径
- `-XX:+ExitOnOutOfMemoryError`:OOM 后退出,方便容器重启
- `-Xlog:gc*`(JDK 9+)或 `-XX:+PrintGCDetails -XX:+PrintGCTimeStamps`
- `-XX:NativeMemoryTracking=detail`(NMT)
- **运行时工具**:
- `jstat -gcutil <pid> 1000`:GC 频率与堆使用率
- `jcmd <pid> GC.heap_info`:堆信息
- `jcmd <pid> VM.native_memory summary`:NMT 原生内存
- `jmap -histo:live <pid>`:对象直方图
- **可视化**:MAT(Dominator Tree / Leak Suspects)、VisualVM、JProfiler、JFR、Arthas `dashboard`
- **生产监控**:Prometheus + JVMExporter、Grafana 大盘、告警(堆使用率 >80%、FullGC 频率、Metaspace 使用率)
5. **解决手段矩阵**
| 问题 | 短期处置 | 长期解决 |
|------|---------|---------|
| 堆溢出 | kill -9 + 换实例、扩堆 | 修内存泄漏、优化分配 |
| Metaspace 溢出 | 调大 `-XX:MaxMetaspaceSize` | 排查 ClassLoader 泄漏 |
| DirectBuffer 溢出 | kill + 重启 | 显式释放、JDK 9+ Cleaner |
| GC overhead | 扩堆、换 GC 器 | 修泄漏、优分配 |
| 线程过多 | kill 线程、扩容 | 线程池化、ThreadLocal 管理 |
6. **实战案例:Netty 内存泄漏**
- 现象:`java.lang.OutOfMemoryError: Direct buffer memory`
- 排查:`jcmd <pid> VM.native_memory summary` 看 DirectByteBuffer 增长
- 定位:检查 `ByteBuf.release()` 是否被调用、`ReferenceCountUtil.safeRelease`
- 解决:Netty 4.1+ 用 `ByteBuf.refCnt()` 检查,或改用 `ReferenceCounted` 自动清理
**考察方向**:
- **深度**:DirectBuffer 泄漏排查、ClassLoader 泄漏、JFR 事件分析
- **广度**:JDK 9+ Metaspace 特性、ZGC 对 OOM 的影响、Loom 协程 OOM
- **应用**:生产 OOM 应急流程、heap dump 分析、监控告警设置
**延伸递进面试题**:
1. **Java heap space 和 GC overhead limit exceeded 有什么关系?**
- 解答要点:GC overhead 通常是堆溢出的前兆——GC 努力回收但效果差;先出现 GC overhead,随后堆溢出
2. **如何判断是内存泄漏还是正常的堆使用?**
- 解答要点:多次 FullGC 后堆使用率仍不下降就是泄漏;MAT 中 Dominator Tree 找"存活最多"的根对象;Shortest Path to GC Roots 看是否被不该引用的强引用持有
3. **Metaspace 溢出如何定位 ClassLoader 泄漏?**
- 解答要点:`jcmd <pid> GC.class_histogram` 看 ClassLoader 数量;查热部署、脚本引擎、反射动态代理;用 `VM.classloader_stats` 事件
4. **DirectByteBuffer 泄漏怎么查?**
- 解答要点:JDK 9+ 用 Cleaner 自动回收;JDK 8 靠 PhantomReference;`jcmd VM.native_memory`、JFR `jdk.DirectByteBuffer` 事件;Netty 用 `ByteBuf.refCnt()`
5. **生产 OOM 的应急 SOP 是什么?**
- 解答要点:kill 前 `-XX:+HeapDumpOnOutOfMemoryError` 保证 dump;kill -15 而非 -9;保留 dump 做 MAT 分析;扩堆作为临时措施;根因排查优先修泄漏
面试官您好,OOM 的本质就是**JVM 认为无法通过 GC 解决内存不足**,抛出 `java.lang.OutOfMemoryError`。
**OOM 完整分类**(HotSpot 常见 8 种):
1. **Java heap space**:堆溢出,最常见
2. **GC overhead limit exceeded**:GC 时间 >98% 但回收 <2%,通常是堆溢出的前兆
3. **Metaspace**:元空间溢出(JDK 8+,替代永久代)
4. **Direct buffer memory**:直接内存溢出,Netty/NIO 场景常见
5. **unable to create new native thread**:线程过多导致栈溢出
6. **Requested array size exceeds VM limit**:一次申请超大数组
7. **Compressed class space**:压缩类空间满
8. **unable to make continuation**:Java 21+ Loom 协程溢出
**典型触发场景**:
- **堆溢出**:静态集合无界增长、缓存无 TTL、大对象一次性加载、监听器未反注册、ThreadLocal 未 remove
- **Metaspace 溢出**:CGLIB/ByteBuddy 动态代理、Groovy/JS 脚本反复编译、热部署反复创建 ClassLoader
- **DirectBuffer 溢出**:Netty `ByteBuf` 未 `release()`、`FileChannel.map()` 未 `unmap`
- **线程过多**:Thread 未复用、线程池无上限、ThreadLocal 泄漏导致线程膨胀
**监控手段**:
- **JVM 参数(救命组合)**:`-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path` 让 OOM 时自动 dump 堆,`-XX:+ExitOnOutOfMemoryError` 让容器快速重启,`-Xlog:gc*` 打完整 GC 日志,`-XX:NativeMemoryTracking=detail` 开 NMT 跟踪原生内存
- **运行时工具**:`jstat -gcutil <pid> 1000` 看 GC 频率、`jcmd <pid> VM.native_memory summary` 看原生内存、`jmap -histo:live <pid>` 看对象直方图
- **可视化分析**:MAT 打开 dump 看 Dominator Tree 和 Leak Suspects,VisualVM、JProfiler、JFR、Arthas `dashboard`
- **生产监控**:Prometheus + JVMExporter 打点,Grafana 大盘,告警规则:堆使用率 >80%、FullGC 频率、Metaspace 使用率、DirectBuffer 使用率
**排查思路**:
1. 先看 OOM 类型(异常消息)
2. 打开 dump,MAT 看 Dominator Tree 找最大对象持有者
3. 看 Shortest Path to GC Roots 判断是不是被"不该引用的强引用"持有
4. 如果是 Metaspace,用 `GC.class_histogram` 看 ClassLoader 数量
5. 如果是 DirectBuffer,用 `VM.native_memory` 或 JFR 事件分析
**解决矩阵**:
- **堆溢出**:短期扩堆+重启,长期修泄漏(缓存加 TTL、连接池设上限、监听器反注册、ThreadLocal 用弱引用)
- **Metaspace 溢出**:调大 `-XX:MaxMetaspaceSize`,排查 ClassLoader 泄漏
- **DirectBuffer 溢出**:显式 `release()`、JDK 9+ Cleaner、`-XX:MaxDirectMemorySize` 上限
- **GC overhead**:扩堆+优化分配+换 GC 器(G1/ZGC)
- **线程过多**:线程池化,ThreadLocal 管理
一个实战案例:Netty 服务 `Direct buffer memory` OOM。现象是堆没满但进程挂掉。排查用 `jcmd VM.native_memory` 看 DirectByteBuffer 线性增长,定位到某个 handler 没 `release()` 掉 ByteBuf。修复:Netty 4.1+ 用 `ReferenceCountUtil.safeRelease`,JFR 加 `jdk.DirectByteBuffer` 事件监控。
一句话总结:**OOM 排查三件套——堆 dump + GC 日志 + 原生内存追踪**,配合 METASPACE 泄漏和 DirectBuffer 泄漏的专项排查,就能覆盖 95% 的生产 OOM 场景。
---
**核心考点定位**:考察 GC 器选型的**决策框架**(业务场景 + JDK 版本 + 内存规模 + 权衡三角)+ **实战参数调优**+ **评估手段**。
**知识点拆解**:
1. **GC 演进主线**
- **Serial**(1996):单线程,为快而生,适合小堆单核
- **ParallelGC**(JDK 5+):并行化标记-整理,追求吞吐,JDK 8 默认
- **CMS**(JDK 5+):并发标记-清除,追求停顿,但有碎片+Concurrent Mode Failure,JDK 14 弃用
- **G1**(JDK 7+,JDK 9 默认):Region 结构+可预测停顿,通用型推荐
- **ZGC / Shenandoah**(JDK 15+):读屏障+多映射,STW <10ms,TB 级堆
2. **GC 选择的核心权衡三角**:**吞吐 vs 停顿 vs 空间**——三者不可兼得
- 批处理 → 吞吐优先(ParallelGC)
- Web 服务 → 停顿优先(G1、ZGC)
- 嵌入式/小堆 → 空间优先(Serial)
3. **决策依据**(三层漏斗)
- **第一层:JDK 版本**——低版本用不了新 GC 器(如 JDK 8 用不了 ZGC)
- **第二层:内存规模**——G1 4-32GB 最佳,ZGC 4GB+,Serial 适合 <1GB
- **第三层:业务特征**——吞吐型 vs 延迟型 vs 通用型
4. **G1 的核心优势**
- **Region 结构**:把堆切成等大小 Region(1MB-32MB),回收选最脏的 Region 优先,避免整堆扫描
- **可预测停顿**:`-XX:MaxGCPauseMillis` 目标停顿,G1 自动决定每次回收多少 Region
- **Mixed GC**:Young + 部分老年代 Region,避免退化到 Full GC
- **无碎片**:Region 内可整理
5. **ZGC 的核心优势**
- **读屏障+多映射**:STW 只有 Initial Mark 和 Final Relocate 两瞬间,都 <1ms
- **堆 8MB-16TB**:大堆首选
- **堆外内存感知**:JDK 21+ 可管理 DirectByteBuffer 生命周期
- JDK 21 引入**分代 ZGC**(Young/Old 分区)
6. **实战调优**
- **G1 关键参数**:`-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45`
- **ZGC 关键参数**:`-XX:+UseZGC -Xms -Xmx`(建议相等)
- **堆大小**:`-Xms=-Xmx`(避免动态扩缩开销)
- **元空间**:`-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m`
7. **GC 评估手段**
- **GC 日志**:`-Xlog:gc*`(JDK 9+)、`-XX:+PrintGCDetails`(JDK 8)
- **JFR**:`jdk.GCPhasePause`、`jdk.GCHeapSummary`、`jdk.Safepoint`
- **Prometheus + JVMExporter**:Grafana 大盘
- **关键指标**:GC 频率、STW 时长分布、FullGC 次数、Metaspace 使用率、GC 后堆占用
8. **项目场景选型参考表**
| 场景 | 堆大小 | JDK 版本 | 推荐 GC |
|------|-------|---------|---------|
| 嵌入式/单核 | <1GB | 任意 | Serial |
| 批处理/后台 | 4-32GB | JDK 8+ | ParallelGC |
| Web 服务通用 | 4-32GB | JDK 8+ | G1(推荐) |
| Web 服务低延迟 | 4-32GB | JDK 15+ | ZGC |
| 大堆服务 | 32GB-16TB | JDK 15+ | ZGC |
| 需要并发优先 | 32GB+ | JDK 11+ | Shenandoah |
**考察方向**:
- **深度**:G1 的 Region 机制、ZGC 的多映射原理、GC 参数调优
- **广度**:JDK 版本演进(JDK 8→11→17→21)、LTS 版本策略、ZGC 分代演进
- **应用**:GC 日志分析、生产 GC 问题排查、GC 器迁移(如 ParallelGC→G1)
**延伸递进面试题**:
1. **G1 的 Region 机制和传统分代有什么区别?**
- 解答要点:G1 是**逻辑分代**(Region 标记为 Young/Old/Humongous),不是物理分代;回收选任意 Region,避免整堆扫描;`-XX:G1HeapRegionSize` 调 Region 大小
2. **ZGC 如何实现 <10ms 停顿?**
- 解答要点:并发标记+并发整理+读屏障+多映射;对象移动时改映射而非改引用;STW 只有 Initial Mark 和 Final Relocate 两瞬间
3. **如何在生产上评估 GC 是否合适?**
- 解答要点:GC 日志 `-Xlog:gc*`、JFR `jdk.GCPhasePause`、Prometheus + JVMExporter;关键指标:GC 频率、STW 时长 P99、FullGC 次数、堆使用率
4. **JDK 8 升级到 JDK 17 需要注意什么?**
- 解答要点:CMS 已弃用(JDK 14 移除)必须换 G1/ZGC;模块系统(JPMS);Finalization 弃用改 Cleaner;JFR 事件更多;`-Xlog` 替代 `-XX:+Print*`
5. **什么时候从 G1 换 ZGC?**
- 解答要点:堆 >32GB、P99 STW 敏感(如支付/实时交易)、JDK 15+、可接受略低吞吐;换 ZGC 前要压测对比吞吐和延迟
面试官您好,GC 器选型本质上是在**吞吐 vs 停顿 vs 空间**的权衡三角里做取舍,实际选型我用**三层漏斗**决策。
**第一层是 JDK 版本**。低版本用不了新 GC 器——JDK 8 用不了 ZGC,JDK 14 之后 CMS 被移除。这是硬约束。
**第二层是内存规模**。Serial 适合小堆单核(<1GB),G1 在 4-32GB 最佳,ZGC 从 4GB 起也能跑,堆可到 16TB。
**第三层是业务特征**。批处理追求吞吐用 ParallelGC,Web 服务追求停顿用 G1,低延迟+大堆用 ZGC。
**GC 演进主线**:Serial(单线程,为快)→ ParallelGC(并行化,求吞吐,JDK 8 默认)→ CMS(并发标记-清除,求停顿,但有碎片+Concurrent Mode Failure,JDK 14 弃用)→ G1(Region 结构+可预测停顿,JDK 9+ 默认)→ ZGC/Shenandoah(读屏障+多映射,STW <10ms,TB 级堆,JDK 15+)。
**我们项目用的是 G1**,理由是三层漏斗刚好命中:JDK 8 支持 G1,堆 16-32GB 在 G1 最佳区间,Web 服务通用场景停顿优先。G1 的核心优势是**Region 结构**(堆切成等大小 Region,回收选最脏的优先)+**可预测停顿**(`-XX:MaxGCPauseMillis=200`)+**Mixed GC 避免退化 FullGC**。配置简单、稳定性好、社区成熟。
**ZGC 为什么暂时不用**?我们 JDK 8 用不了(ZGC JDK 15 GA),堆也没到需要 ZGC 的规模(32GB+)。等升级 JDK 17/21 时再评估——特别是如果 P99 停顿敏感或堆超过 32GB,ZGC 的 <10ms 停顿就是巨大优势。
**核心调优参数**:`-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -Xms=Xmx`(避免动态扩缩)。堆大小建议 Xms=Xmx 减少扩缩开销。
**怎么评估 GC 是否合适?** 关键看三个指标:**GC 频率**(每秒数次 YoungGC 正常,FullGC 应极少)、**STW 时长 P99**(生产要求 <200ms)、**堆使用率**(FullGC 后仍 >70% 说明有泄漏)。工具用 `-Xlog:gc*` 打日志、JFR `jdk.GCPhasePause` 事件、Prometheus + JVMExporter 打 Grafana 大盘。
**一句话总结**:**GC 器没有最好的,只有最适合的**——按 JDK 版本定可用范围,按内存规模定候选,按业务特征定取舍,最后靠 GC 日志和 JFR 数据验证。G1 是绝大多数 Web 服务的默认答案,ZGC 是大堆低延迟的进阶选择。
---