📅 2026-07-23 | 🏷️ Java · 后端方向 | ⏱️ 建议 5h | 🎯 JVM 是 Java 面试的"试金石"——深度和广度直接决定评级
📌 今日知识地图
JVM 面试全景
│
├── 模块一:运行时数据区(内存模型)
│ ├── 堆(Young/Old) / 元空间 / 虚拟机栈 / 本地方法栈 / 程序计数器
│ ├── 对象创建全流程 & 内存分配(TLAB)
│ └── 对象访问定位(句柄池 vs 直接指针)
│
├── 模块二:垃圾回收(GC)
│ ├── 判断算法:引用计数 / 可达性分析(GC Roots)
│ ├── 四种引用:强/软/弱/虚 + ReferenceQueue
│ ├── GC 算法:标记-清除 / 标记-复制 / 标记-整理
│ └── 分代收集理论
│
├── 模块三:垃圾回收器演进
│ ├── Serial / ParNew / Parallel Scavenge(年轻代)
│ ├── CMS(老年代,并发标记清除)
│ ├── G1(Region + Remember Set + SATB)
│ └── ZGC(染色指针 + 并发整理,亚毫秒停顿)
│
├── 模块四:类加载机制
│ ├── 类加载的 7 个阶段
│ ├── 双亲委派模型 & 破坏方式(SPI/Tomcat/热部署)
│ └── 类加载器:Bootstrap / Extension / Application / 自定义
│
├── 模块五:JIT 编译 & 调优实战
│ ├── JIT:C1/C2 / 逃逸分析 / 标量替换 / 栈上分配
│ ├── 常用 GC 参数 & 日志分析
│ └── OOM 排查:jmap/MAT/jstack/Arthas
│
└── 面试题精选(10 道 + 公司标签)
模块一:运行时数据区
1.1 完整内存布局
JVM 运行时数据区
┌─────────────────────────────────────────────┐
│ 堆 (Heap) │
│ ┌─────────────────────────────────────────┐ │
│ │ 新生代 (Young Gen) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────────────┐ │ │
│ │ │ Eden │ │ S0 │ │ S1 (Survivor)│ │ │
│ │ │ 8/10 │ │ 1/10 │ │ 1/10 │ │ │
│ │ └──────┘ └──────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ 老年代 (Old Gen) │ │
│ └─────────────────────────────────────────┘ │
├─────────────────────────────────────────────┤
│ 元空间 (Metaspace) │
│ (JDK 8 替代永久代) │
│ 存放类元信息、方法、常量池 │
│ 使用本地内存,默认无上限 │
├─────────────────────────────────────────────┤
│ 线程私有区域(每个线程一份,随线程创建/销毁) │
│ ┌──────────┐ ┌──────┐ ┌──────────────┐ │
│ │虚拟机栈 │ │程序计│ │本地方法栈 │ │
│ │(栈帧: │ │数器 │ │(Native方法) │ │
│ │ 局部变量表│ │(记录 │ │ │ │
│ │ 操作数栈 │ │执行位│ │ │ │
│ │ 动态链接 │ │置) │ │ │ │
│ │ 返回地址) │ │ │ │ │ │
│ └──────────┘ └──────┘ └──────────────┘ │
└─────────────────────────────────────────────┘
| 区域 | 线程 | 存储内容 | 异常 |
|---|---|---|---|
| 堆 | 共享 | 对象实例、数组 | OOM: Java heap space |
| 元空间 (1.8+) | 共享 | 类元信息、运行时常量池 | OOM: Metaspace |
| 虚拟机栈 | 私有 | 栈帧(局部变量表/操作数栈/动态链接/返回地址) | StackOverflowError / OOM |
| 本地方法栈 | 私有 | Native 方法的栈帧 | 同虚拟机栈 |
| 程序计数器 | 私有 | 当前线程执行字节码的行号 | 无异常(唯一无 OOM 的区域) |
| 直接内存 | — | NIO 的 DirectByteBuffer | OOM(不受 -Xmx 限制!) |
1.2 对象创建全流程(面试手写题!)
一个 new Person() 经历了什么:
1. 类加载检查
→ 检查常量池中是否有 Person 的符号引用 → 是否已加载/解析/初始化
→ 没有则先执行类加载
2. 分配内存
→ 计算对象大小 → 从堆中划分空间
→ 指针碰撞(Bump the Pointer):内存规整时,指针后移
→ 空闲列表(Free List):内存不规整时,从列表中找
→ 并发安全:CAS + TLAB(Thread Local Allocation Buffer)
3. 初始化零值
→ 将分配的内存空间初始化为零值(保证实例字段不赋初值也能用)
4. 设置对象头(Object Header)
→ Mark Word:哈希码、GC 分代年龄、锁状态标志、偏向线程 ID
→ 类型指针:指向方法区中类的元数据
5. 执行 <init> 方法
→ 按代码中赋值顺序初始化实例字段
→ 执行构造器中的代码
// 对象头结构(64 位 JVM)
// Mark Word (64 bits):
// - 无锁状态:unused(25) | hashcode(31) | unused(1) | age(4) | biased_lock(1) | lock(2)
// - 偏向锁: thread_id(54) | epoch(2) | unused(1) | age(4) | biased_lock(1) | lock(2)
// - 轻量级锁:ptr_to_lock_record(62) | lock(2)
// - 重量级锁:ptr_to_monitor(62) | lock(2)
// - GC 标记: forward_ptr(62) | lock(2)
// 类型指针 (Klass Pointer):默认 64 bits,开启 -XX:+UseCompressedClassPointers 后 32 bits
1.3 对象访问定位
句柄池访问:
reference → 句柄池 → [实例数据指针 | 类型数据指针]
→ 堆中的实例数据
→ 方法区中的类型数据
优点:对象移动时只需改句柄池,reference 不变
缺点:多一次指针跳转
直接指针访问(HotSpot 采用):
reference → 对象 → [对象头 | 实例数据] + 类型指针 → 方法区类型数据
优点:一次指针跳转,快
缺点:对象移动时需要更新 reference(GC 时处理)
1.4 栈帧结构
一个方法调用 = 一个栈帧入栈,方法返回 = 栈帧出栈
栈帧结构:
┌──────────────────┐
│ 局部变量表 │ ← 方法参数 + 局部变量,基本类型存值,引用存指针
│ (Local Vars) │ 槽位可复用(slot),32 位一个槽,long/double 占两个
├──────────────────┤
│ 操作数栈 │ ← JVM 基于栈的指令集,每次 push/pop 操作
│ (Operand Stack) │
├──────────────────┤
│ 动态链接 │ ← 指向运行时常量池中该方法的符号引用
│ (Dynamic Link) │
├──────────────────┤
│ 返回地址 │ ← 调用者的 PC 值(方法正常返回 / 异常返回)
│ (Return Address)│
└──────────────────┘
模块二:垃圾回收(GC)
2.1 判断对象存活
// ═══════════════════════════════════════
// 引用计数法
// ═══════════════════════════════════════
// 每个对象维护引用计数,为 0 则回收
// 缺点:解决不了循环引用(A→B, B→A,计数都不为 0)
// 早期 Python / Redis 用引用计数 + 标记清除补充
// ═══════════════════════════════════════
// 可达性分析(Java/JVM 采用)
// ═══════════════════════════════════════
// 从 GC Roots 出发,沿引用链标记可达对象,不可达的回收
// GC Roots 包括:
// ① 虚拟机栈中引用的对象(局部变量表中的引用)
// ② 方法区中静态属性引用的对象
// ③ 方法区中常量引用的对象
// ④ 本地方法栈中 JNI 引用的对象
// ⑤ 被 synchronized 持有的对象
// ⑥ JVM 内部的引用(Class 对象、常驻异常对象等)
2.2 四种引用
| 引用类型 | 回收时机 | 用途 |
|---|---|---|
| 强引用 (Strong) | 永不回收(除非引用断开) | Object o = new Object() |
| 软引用 (Soft) | 内存不够时回收(OOM 之前) | 图片缓存、网页缓存 |
| 弱引用 (Weak) | 下一次 GC 必定回收 | ThreadLocal 的 Entry、WeakHashMap |
| 虚引用 (Phantom) | 任何时候都可能回收 | 对象被回收前收到通知(管理直接内存) |
// 软引用:内存不够才回收
SoftReference<byte[]> softRef = new SoftReference<>(new byte[100 * 1024 * 1024]);
byte[] data = softRef.get(); // 内存充足 → 返回数据;内存不足 → 返回 null
// 弱引用:GC 必回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc();
Object obj = weakRef.get(); // null — GC 后必为 null
// 虚引用:get() 永远返回 null,需要配合 ReferenceQueue 使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);
// 对象被回收时,虚引用被加入 queue → 收到通知 → 清理资源
2.3 GC 算法
| 算法 | 过程 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 标记-清除 | 标记可达→清除不可达 | 简单 | 碎片多 | 老年代(CMS 的基础) |
| 标记-复制 | 分两块,存活对象从一块复制到另一块,清空原块 | 无碎片 | 浪费一半空间 | 新生代 |
| 标记-整理 | 标记可达→存活对象移到一端→清理边界外 | 无碎片 | 耗时(移动对象) | 老年代 |
2.4 分代收集理论
分代假设(两个):
1. 弱分代假说:绝大多数对象朝生夕死 → 新生代高频 GC(Minor GC)
2. 强分代假说:熬过越多次 GC 的对象越难死 → 老年代低频 GC(Major/Full GC)
新生代分配与晋升:
新对象 → Eden
Eden 满了 → Minor GC → 存活对象 → S0
下次 Minor GC → Eden + S0 存活对象 → S1(S0 和 S1 交替使用)
每熬过一次 Minor GC → 年龄 +1 → 年龄达到阈值(默认 15)→ 晋升老年代
跨代引用怎么办?
→ 老年代引用新生代对象
→ Remembered Set:老年代维护一个"卡表"(Card Table)
→ 新生代 GC 时只需扫描"脏卡"对应的老年代区域,不需要扫整个老年代
模块三:垃圾回收器演进
3.1 七种回收器速览
新生代回收器: 老年代回收器:
Serial ───→ Serial Old(单线程,标记-整理)
ParNew ───→ CMS(并发标记清除)
Parallel Scavenge ─→ Parallel Old(吞吐量优先)
全区域回收器:
G1(Region 分代 + 并发整理)
ZGC(染色指针 + 全并发,亚毫秒)
Shenandoah(全并发,无分代)
连线规则(经典组合):
Serial + Serial Old ← 单线程,Client 模式
ParNew + CMS ← 经典组合,低延迟
Parallel Scavenge + Parallel Old ← 吞吐量优先
G1 ← JDK 9+ 默认
ZGC ← JDK 15+ 生产可用,大内存低延迟
3.2 各回收器核心特点
CMS(Concurrent Mark Sweep)— 低延迟经典
CMS 四个阶段:
1. 初始标记(STW,极短):只标记 GC Roots 直接引用的对象
2. 并发标记(无 STW):沿着引用链标记,与用户线程并发
3. 重新标记(STW,较短):修正并发标记期间的变动(三色标记 + SATB)
4. 并发清除(无 STW):清除未标记的对象
CMS 三大问题(面试必问!):
① 并发时用户线程还在产生垃圾 → "浮动垃圾" → 需要预留空间
② 标记-清除 → 碎片 → 碎片太多导致 Concurrent Mode Failure → 退化为 Serial Old
③ 对 CPU 敏感(并发阶段占用线程资源)
G1(Garbage First)— JDK 9+ 默认
G1 核心概念:
Region:把堆划分为 2048 个大小相等的 Region(每个 1~32MB)
每个 Region 可以是 Eden / Survivor / Old / Humongous
G1 的混合回收(Mixed GC):
不只是回收新生代,还回收部分老年代 Region
"Garbage First" = 优先回收垃圾最多的 Region
Remembered Set(RSet):
每个 Region 维护 RSet → 记录"谁引用了我"
避免了全堆扫描
SATB(Snapshot-At-The-Beginning):
在并发标记开始时拍快照 → 标记期间新产生的对象默认存活
G1 GC 周期:
Young GC(只回收新生代 Region):
→ STW,复制存活对象到 Survivor/Old Region
→ 常规操作
Mixed GC(回收新生代 + 部分老年代):
1. 初始标记(STW,跟 Young GC 一起做)
2. 根区域扫描(并发)
3. 并发标记(无 STW)
4. 重新标记(STW)
5. 清理(STW,筛选回收价值最高的 Region)
6. 复制(STW,将选定 Region 的存活对象移到空闲 Region)
Full GC(万不得已):
→ Mixed GC 跟不上分配速度 → STW 的标记-整理 → 慢!
→ 调优目标:避免 Full GC
ZGC — 亚毫秒级停顿
ZGC 核心创新:染色指针(Colored Pointers)
在 64 位指针中嵌入元数据(前 4 位):
- Finalizable:对象只能被 Finalizer 访问
- Remapped:指向对象的最新地址
- Marked 0/1:标记位
技术特点:
→ 并发整理:移动对象时用"读屏障"自愈指针
→ 全阶段并发:初始标记和重新标记也是并发的(仅极短 STW)
→ STW < 1ms(不管堆多大)
→ JDK 15 生产可用,JDK 21 支持分代 ZGC
3.3 回收器选型指南
| 场景 | 推荐回收器 | 理由 |
|---|---|---|
| 堆 < 4G,低延迟 | ParNew + CMS | 经典方案,但 CMS 已废弃(JDK 14) |
| 堆 4G ~ 32G,平衡 | G1 | JDK 9+ 默认,最通用的选择 |
| 堆 > 32G,极低延迟 | ZGC | 亚毫秒停顿,大堆首选 |
| 吞吐量优先 | Parallel Scavenge + Parallel Old | 后台计算任务 |
| 单核 / 小内存 | Serial + Serial Old | Client 模式 |
模块四:类加载机制
4.1 类加载的 7 个阶段
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
① 加载:通过全限定名获取二进制字节流 → 转为方法区数据结构 → 生成 Class 对象
② 验证:文件格式验证 / 元数据验证 / 字节码验证 / 符号引用验证
③ 准备:为静态变量分配内存并赋零值(不包含代码中的赋值)
例外:static final 常量在准备阶段就赋代码中的值
④ 解析:将常量池中的符号引用替换为直接引用
⑤ 初始化:执行类构造器 <clinit>() 方法
包含:静态变量赋值语句 + static {} 代码块
顺序:按代码出现顺序,父类先于子类
// 准备 vs 初始化的区别
class Demo {
static int a = 10; // 准备阶段:a = 0;初始化阶段:a = 10
static final int b = 20; // 准备阶段:b = 20(常量,编译期就确定了)
static { /* 初始化代码 */ }
}
4.2 双亲委派模型
双亲委派:一个类加载器收到类加载请求时,先委托给父加载器,父加载器加载不了才自己加载。
目的:
→ 保证 Java 核心类的安全:不管谁加载,String 始终是 Bootstrap ClassLoader 的那个 String
→ 保证同一个类不被重复加载
三层类加载器:
Bootstrap ClassLoader(启动类加载器)
↑ 加载 <JAVA_HOME>/lib 下的核心类(rt.jar, modules)
↑ 用 C++ 实现,Java 中返回 null
Extension / Platform ClassLoader(扩展/平台类加载器)
↑ 加载 <JAVA_HOME>/lib/ext 或 java.ext.dirs
Application ClassLoader(应用类加载器)
↑ 加载 classpath 下的类
自定义 ClassLoader
4.3 破坏双亲委派的三种情况
// ═══════════════════════════════════════
// 破坏方式一:SPI(Service Provider Interface)
// ═══════════════════════════════════════
// 问题:核心类(如 java.sql.DriverManager)由 Bootstrap 加载
// 但它的实现类(如 com.mysql.jdbc.Driver)在 classpath 中
// Bootstrap 加载不到 → 需要子加载器来加载
// 解决:线程上下文类加载器(Thread Context ClassLoader)
// DriverManager 通过 TCCL 获取 App ClassLoader 来加载实现类
// ═══════════════════════════════════════
// 破坏方式二:Tomcat 类加载
// ═══════════════════════════════════════
// 需求:① 不同 Web 应用隔离(各用各的 lib)
// ② 共享公共库(Servlet API 等)
// ③ 支持热部署(替换应用而不用重启 Tomcat)
// Tomcat 方案:每个 Web 应用有独立的 WebAppClassLoader
// → 优先自己加载 → 自己加载不到才委托给父加载器
// ═══════════════════════════════════════
// 破坏方式三:热部署 / OSGi
// ═══════════════════════════════════════
// 每个模块有自己的 ClassLoader → 卸载模块时直接丢弃其 ClassLoader
模块五:JIT 编译 & 调优实战
5.1 JIT 编译器
// JIT (Just-In-Time) 编译器:将热点代码编译为本地机器码
// 两种编译器:
// C1(Client Compiler):编译快,优化少 → 适合 GUI 应用
// C2(Server Compiler):编译慢,优化激进 → 适合长时间运行的服务端
// 分层编译(Tiered Compilation,JDK 8+ 默认开启):
// Level 0:解释执行
// Level 1:C1 编译,无 profiling
// Level 2:C1 编译,带简单 profiling
// Level 3:C1 编译,带完整 profiling
// Level 4:C2 编译,基于 profiling 数据做激进优化
// 热点代码检测:
// 方法调用计数器:方法被调用超过阈值 → JIT 编译
// 回边计数器:循环执行次数超过阈值 → OSR (On Stack Replacement)
5.2 逃逸分析
// 逃逸分析:分析对象的作用范围
// 对象不逃逸出方法 → 可以栈上分配、标量替换、锁消除
public String createString() {
StringBuilder sb = new StringBuilder(); // sb 不逃逸出方法
sb.append("hello");
return sb.toString();
// 优化:sb 可以直接在栈上分配(不占堆内存),甚至标量替换(拆成字段在寄存器中操作)
}
// 三种优化:
// ① 栈上分配:对象在栈帧中分配,方法结束自动释放,无需 GC
// ② 标量替换:将对象拆解为其成员字段,直接在 CPU 寄存器/栈上操作
// ③ 同步消除:不逃逸的对象上的锁操作可以被消除
5.3 常用 GC 参数
# ═══════════════════════════════════════
# 堆大小
# ═══════════════════════════════════════
-Xms2g # 初始堆大小
-Xmx4g # 最大堆大小(建议跟 -Xms 相同,避免堆伸缩)
-Xmn1g # 新生代大小
-XX:NewRatio=2 # 老年代/新生代比例 = 2
-XX:SurvivorRatio=8 # Eden/Survivor 比例 = 8
# ═══════════════════════════════════════
# 元空间
# ═══════════════════════════════════════
-XX:MetaspaceSize=128m # 元空间初始大小
-XX:MaxMetaspaceSize=256m # 元空间最大大小
# ═══════════════════════════════════════
# GC 选择
# ═══════════════════════════════════════
-XX:+UseG1GC # 使用 G1
-XX:+UseZGC # 使用 ZGC(JDK 15+)
-XX:MaxGCPauseMillis=200 # G1 目标停顿时间(ms)
# ═══════════════════════════════════════
# GC 日志(JDK 9+ 统一日志格式)
# ═══════════════════════════════════════
-Xlog:gc*:file=gc.log:time,tags,level:filecount=10,filesize=100M
# ═══════════════════════════════════════
# 内存溢出 dump
# ═══════════════════════════════════════
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
# ═══════════════════════════════════════
# 其他常用
# ═══════════════════════════════════════
-XX:+PrintGCDetails # 打印 GC 详情(JDK 8)
-XX:+UseCompressedOops # 压缩普通对象指针
-XX:+UseCompressedClassPointers # 压缩类指针
5.4 OOM 排查实战
# ═══════════════════════════════════════
# 第一步:拿到 heap dump
# ═══════════════════════════════════════
# 方式一:启动时配置自动 dump
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
# 方式二:手动 dump
jmap -dump:live,format=b,file=dump.hprof <pid>
# ═══════════════════════════════════════
# 第二步:MAT 分析
# ═══════════════════════════════════════
# 1. 打开 hprof 文件 → Leak Suspects Report
# 2. 看 Dominator Tree:哪个类占最多内存
# 3. 看 GC Roots 路径:这个对象为什么没被回收
# 4. 常见结论:
# - HashMap 无限增长 → 内存泄漏
# - ThreadLocal 没 remove → 内存泄漏
# - 大对象(byte[])太多 → 合理还是异常
# - 类加载器太多 → Metaspace OOM
# ═══════════════════════════════════════
# 第三步:线程排查
# ═══════════════════════════════════════
jstack <pid> # 查看线程栈
jstack <pid> | grep "BLOCKED" # 找死锁
top -H -p <pid> # 找 CPU 最高的线程
# printf '%x\n' <tid> → 转十六进制 → 在 jstack 中搜索 nid=0x<tid>
# ═══════════════════════════════════════
# 第四步:Arthas 在线诊断(神器!)
# ═══════════════════════════════════════
dashboard # 实时监控
thread # 线程状态
jad com.example.Foo # 反编译类
watch com.example.Foo bar '{params, returnObj}' # 观测方法调用
trace com.example.Foo bar # 方法调用路径 + 耗时
5.5 四大 OOM 场景
| OOM 类型 | 信息 | 常见原因 | 排查 |
|---|---|---|---|
| Java heap space | 堆内存不足 | 内存泄漏/大对象/配置太小 | jmap + MAT |
| Metaspace | 元空间不足 | 动态生成大量类/配置太小 | 增大 MaxMetaspaceSize |
| GC overhead limit exceeded | GC 时间超过 98% 但回收不到 2% | 堆太小/内存泄漏 | 增大堆或排查泄漏 |
| Direct buffer memory | 直接内存不足 | NIO 未释放 ByteBuffer | 排查 DirectByteBuffer |
面试题精选(10 道)
Q1. JVM 运行时数据区有哪些?(字节/阿里/美团 最高频)
标准回答:
线程共享:堆(对象实例)、元空间(类元信息、常量池,JDK 8 替代永久代)。
线程私有:虚拟机栈(栈帧包含局部变量表/操作数栈/动态链接/返回地址)、本地方法栈(Native 方法)、程序计数器(记录当前执行指令地址,唯一不抛 OOM 的区域)。
直接内存不在 JVM 内存模型内,但受 -XX:MaxDirectMemorySize 控制,NIO 使用。
Q2. 对象创建的全过程?(字节/阿里 高频)
标准回答:
① 类加载检查 → ② 分配内存(指针碰撞/空闲列表,CAS+TLAB 保证并发安全)→ ③ 初始化零值 → ④ 设置对象头(Mark Word + 类型指针)→ ⑤ 执行 <init> 方法(实例字段初始化 + 构造器)。
关键细节:TLAB 为每个线程在 Eden 区预留一小块内存,分配时无需 CAS → 减少竞争。
Q3. 什么是 GC Roots?(阿里/腾讯)
标准回答:
可达性分析的起点,包括:虚拟机栈中局部变量表引用的对象、方法区静态变量引用的对象、常量引用的对象、JNI 引用的对象、synchronized 持有的对象、JVM 内部引用(Class 对象等)。从这些根出发遍历引用链,不可达的对象判定为可回收。
Q4. 四种引用的区别和使用场景?(字节/美团)
标准回答:
强引用永不被回收。软引用在 OOM 前回收(适合缓存)。弱引用在一次 GC 后必回收(ThreadLocal 的 Entry 用弱引用持有 key,防止 key 无法回收)。虚引用 get() 永远返回 null,配合 ReferenceQueue 在对象回收后收到通知(管理直接内存的释放,Cleaner 类)。
Q5. CMS 和 G1 的区别?(阿里/字节 高频)
标准回答:
CMS 是分代回收器(新生代 ParNew + 老年代 CMS),G1 是全区域回收器(Region 分代)。CMS 用标记-清除(有碎片),G1 用标记-复制-整理(无碎片)。CMS 的并发标记用增量更新,G1 用 SATB。CMS 只回收老年代,G1 做 Mixed GC(回收部分老年代 Region)。G1 可预测停顿(-XX:MaxGCPauseMillis),CMS 不可预测。G1 适合 4G+ 堆,CMS 适合小堆低延迟。
Q6. G1 的 Mixed GC 和 Full GC 的区别?(字节/阿里)
标准回答:
Mixed GC 回收新生代 + 部分老年代 Region(根据暂停时间目标选择回收价值最高的 Region),大部分阶段并发执行,STW 时间可控。Full GC 是单线程的标记-整理(STW),当 Mixed GC 跟不上分配速度时触发,停顿时间长,调优目标是避免 Full GC。
Q7. 类加载的双亲委派模型?为什么要破坏它?(阿里/腾讯 高频)
标准回答:
一个类加载器收到加载请求后先委托给父加载器,父加载器加载不了才自己加载。保证 Java 核心类的安全(同一个 String 类不会出现多个版本)。
三种破坏:① SPI 机制——核心类(Bootstrap 加载)需要调用应用层的实现类,通过线程上下文类加载器(TCCL)回调;② Tomcat——每个 Web 应用独立 ClassLoader,优先自己加载而非委托父类,实现应用隔离和热部署;③ OSGi 热部署——模块化加载,卸载模块时直接丢弃 ClassLoader。
Q8. ZGC 为什么能做到亚毫秒停顿?(腾讯/字节)
标准回答:
ZGC 的核心技术是染色指针(Colored Pointers)和读屏障。在 64 位指针中嵌入 GC 状态信息(Remapped/Marked0/Marked1),并发移动对象时通过读屏障自愈指针——访问对象时如果发现指针状态是旧地址,读屏障自动修正为新地址。所有 GC 阶段(标记、整理、重映射)都和用户线程并发,STW 只在极短的根扫描阶段,因此停顿不受堆大小影响。
Q9. 逃逸分析是什么?做了哪些优化?(阿里/美团)
标准回答:
逃逸分析判断对象的作用范围是否超出方法/线程。不逃逸的对象可以做:① 栈上分配(对象在栈帧中分配,方法结束自动释放);② 标量替换(将对象拆为成员字段分散存储,避免分配对象);③ 同步消除(不逃逸对象上的锁操作可以消除)。JDK 8+ 默认开启逃逸分析,可以通过 -XX:+DoEscapeAnalysis 控制。
Q10. 如何排查 OOM?用过什么工具?(美团/字节)
标准回答:
四步法:① 获取 heap dump(-XX:+HeapDumpOnOutOfMemoryError 或 jmap);② MAT 分析——看 Dominator Tree(谁占最多)、GC Roots 路径(为什么没回收);③ jstack 看线程栈(是否有死锁、大量线程 BLOCKED);④ Arthas 在线诊断——实时 dashboard 看内存/GC/线程,trace 追踪具体方法调用。
常见原因:HashMap 无限增长(没有设置过期/上限)、ThreadLocal 没 remove(线程池复用导致)、大对象(byte[])积累、类加载器泄漏(Metaspace OOM)。
📊 今日知识图谱
JVM 完整体系 DAY 9
│
├── 运行时数据区
│ ├── 共享:堆(Young+Old) + 元空间(类信息/常量池)
│ ├── 私有:栈(栈帧) + 本地方法栈 + 程序计数器
│ └── 对象创建:类检查→分配内存(CAS+TLAB)→零值→对象头→init
│
├── 垃圾回收
│ ├── 判断:可达性分析(GC Roots)
│ ├── 引用:强(不回收)/软(OOM前)/弱(GC必收)/虚(通知)
│ ├── 算法:标记-清除(碎片)/复制(无碎片)/整理(无碎片+耗时)
│ └── 分代:新生代(Eden+S0/S1复制) + 老年代 + 卡表
│
├── 回收器演进
│ ├── Serial(单线程) → ParNew(多线程) → Parallel(吞吐量)
│ ├── CMS(并发清除,碎片,退化为SerialOld,JDK14废弃)
│ ├── G1(Region,RSet,SATB,MixedGC,预测停顿,JDK9+默认)
│ └── ZGC(染色指针,读屏障,全并发,亚毫秒停顿,JDK15+)
│
├── 类加载
│ ├── 7阶段:加载→验证→准备→解析→初始化→使用→卸载
│ ├── 双亲委派:自底向上委托,自顶向下加载
│ └── 破坏:SPI(TCCL) / Tomcat(隔离+热部署) / OSGi
│
└── 调优实战
├── JIT:C1/C2分层编译 / 逃逸分析(栈上分配/标量替换/锁消除)
├── 参数:-Xms/Xmx/Xmn/UseG1GC/MaxGCPauseMillis
└── 排查:jmap(heap dump) → MAT → jstack → Arthas
🔜 明日预告
Day 10 — Java 并发编程完整体系
- 线程生命周期 + synchronized 底层(对象头/锁升级)
- volatile(可见性/禁止重排/不保证原子性)
- AQS 完整拆解(state + CLH 队列 + acquire/release)
- ReentrantLock(公平/非公平)+ Condition
- 线程池 7 参数 + 4 种拒绝策略 + 执行流程
- ThreadLocal(ThreadLocalMap/WeakReference/内存泄漏)
- JUC 工具:CountDownLatch/CyclicBarrier/Semaphore/CompletableFuture
- ConcurrentHashMap 1.8 源码深度解读
- 10 道并发高频真题
💡 速通心法:JVM 面试三条主线——① 内存(运行时数据区每块放什么、OOM 怎么查);② GC(从 Serial 到 ZGC 的演进逻辑、G1 的 Region+RSet+SATB);③ 类加载(双亲委派 + 三种破坏场景)。这三条线能串起来讲 30 分钟不卡壳,JVM 面试就稳了。

389

被折叠的 条评论
为什么被折叠?



