这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章把 JVM 面试中的高频问题整理成可讲清、可追问、可落地的答案。面试的重点不是背诵结论,而是展示边界意识和排查能力。
30.1 JVM 内存区域
问题:JVM 运行时数据区有哪些?
回答要点:
- 堆:对象和数组,线程共享;
- 方法区:类元数据、运行时常量池,HotSpot 现为 Metaspace 实现;
- 虚拟机栈:栈帧、局部变量表、操作数栈;
- 本地方法栈:Native 方法;
- 程序计数器:线程私有,记录当前执行字节码位置。
加分点:
JDK 8 移除永久代,Metaspace 使用本地内存;
虚拟机栈可能 StackOverflowError;
堆和 Metaspace 可能 OOM;
直接内存不属于 JVM 规范定义的堆,但会影响进程总内存。
30.2 对象创建过程
问题:new 一个对象时发生什么?
简化流程:
1. 类加载检查
2. 分配内存
3. 初始化零值
4. 设置对象头
5. 执行 <init> 构造逻辑
追问点:
- 指针碰撞还是空闲列表取决于堆是否规整;
- 分配可能使用 TLAB;
- 对象头包含 Mark Word 和类型指针;
- 大对象可能进入老年代或特殊 Region。
30.3 对象内存布局
问题:对象在堆里如何存储?
结构:
Object Header
Mark Word
Klass Pointer
Instance Data
字段,可能重排
Padding
对齐到 8 字节
压缩指针:
- 开启压缩 OOP 时,Klass Pointer 可能为 4 字节;
- 堆超过一定范围后可能失效;
- 对齐方式可能调整;
- 具体行为与 JVM 版本和参数有关。
30.4 类加载过程
问题:类加载有几个阶段?
回答:
加载 -> 验证 -> 准备 -> 解析 -> 初始化
| 阶段 | 关键动作 |
|---|---|
| 加载 | 读取字节流生成 Class 对象 |
| 验证 | 格式、元数据、字节码、符号引用 |
| 准备 | 静态变量分配并设零值 |
| 解析 | 符号引用转直接引用 |
| 初始化 | 执行 <clinit> |
追问:静态变量在准备阶段是零值,static final int x = 1 这类编译期常量可能在准备阶段直接赋值。
30.5 双亲委派
问题:什么是双亲委派?
流程:
ClassLoader.loadClass
-> 先委托 parent
-> parent 找不到再由当前 loader findClass
作用:
- 避免核心类被重复加载;
- 防止用户替换核心 API;
- 保证类的一致命名空间。
破坏场景:
- SPI 需要父加载器使用子加载器;
- Web 容器隔离应用类;
- OSGi 模块化;
- 热部署和动态代理。
“破坏”不一定是贬义,而是为满足隔离和可见性需求做的设计取舍。
30.6 判断对象存活
问题:JVM 如何判断对象可以回收?
回答:
- 主流 HotSpot 使用可达性分析;
- 从 GC Roots 出发搜索;
- 不可达对象可回收;
- finalize 已废弃,不应作为资源释放方案;
- 引用类型影响回收时机。
常见 Roots:
线程栈局部变量
静态字段
JNI 引用
系统类加载器加载的核心类
活跃线程对象
30.7 引用类型
| 类型 | 回收倾向 | 典型用途 |
|---|---|---|
| 强引用 | 不回收 | 普通对象 |
| 软引用 | 内存不足前回收 | 缓存,现代缓存更常用显式容量 |
| 弱引用 | 下次 GC 回收 | WeakHashMap、ThreadLocalMap |
| 虚引用 | 不影响生命周期 | 跟踪回收、堆外内存清理 |
追问:ThreadLocalMap 的 key 是弱引用,value 是强引用,因此仍需要 remove()。
30.8 GC 算法
问题:常见的 GC 算法有哪些?
回答:
| 算法 | 思路 | 问题 |
|---|---|---|
| 标记-清除 | 标记后直接释放 | 碎片 |
| 标记-复制 | 存活对象复制到另一块 | 空间成本 |
| 标记-整理 | 存活对象向一侧移动 | 移动成本 |
| 分代 | 按生命周期分区 | 跨代引用和复杂度 |
结合收集器说明:Serial、Parallel、G1、ZGC、Shenandoah 是不同工程落地,不是孤立算法名词。
30.9 G1 与 CMS
问题:G1 相比 CMS 有什么优势?
回答:
- G1 是 Region 化布局;
- 可预测停顿目标;
- 标记整理降低碎片;
- Mixed GC 逐步回收 Old;
- CMS 在 JDK 14 被移除。
CMS 的历史问题:
并发标记清除产生碎片
concurrent mode failure 退级 Serial Old
Remark 阶段成本
Remembered Set 维护成本
面试中要说明自己使用的是哪个 JDK,避免把 JDK 8 经验泛化。
30.10 ZGC 特点
问题:ZGC 为什么停顿低?
回答:
- 标记、搬迁大部分并发执行;
- 着色指针记录对象状态;
- 读屏障帮助修正引用;
- 停顿阶段工作量小;
- JDK 21 引入分代模式。
代价:
- 内存开销更高;
- 读屏障影响吞吐;
- 并发阶段消耗 CPU;
- 堆太满时可能 Allocation Stall。
30.11 volatile
问题:volatile 有什么作用?
回答:
- 可见性:写入后其他线程能读到新值;
- 有序性:禁止特定指令重排;
- 不保证原子性。
示例:
private volatile boolean running = true;
public void stop() {
running = false;
}
适合状态标志。volatile int count; count++ 仍不是线程安全的,应使用 AtomicInteger 或锁。
30.12 synchronized 锁升级
问题:synchronized 有哪些锁状态?
简化路径:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
要点:
- JDK 15 起默认废弃偏向锁;
- 锁状态存储在对象头 Mark Word;
- 竞争程度影响升级;
- 锁不一定是“只能升级不能降级”的简单过程;
- JIT 可能锁消除或锁粗化。
回答时补充版本差异,比背固定路径更专业。
30.13 Java 内存模型
问题:JMM 解决什么问题?
回答:
- 定义共享变量的可见性;
- 定义指令重排的边界;
- 定义 happens-before 规则;
- 为 volatile、final、锁提供语义基础。
示例规则:
程序顺序规则
monitor lock 规则
volatile 规则
线程 start/join 规则
传递性
JMM 不是堆栈分区图,面试中不要把内存区域和内存模型混为一谈。
30.14 排查 OOM
问题:线上 OOM 怎么排查?
回答框架:
1. 先确认 OOM 类型
2. 读取完整异常栈
3. 保存 GC 日志和 dump
4. 分析回收后占用
5. MAT 找 Retained Heap 和引用链
6. 结合业务源码确认生命周期
7. 修复后压测验证
加分:
- 区分堆、Metaspace、线程、直接内存;
- 区分 Java OOM 和 OOMKilled;
- 说明 dump 会 STW;
- 说明自动 dump 参数;
- 给出真实案例结构。
30.15 排查 CPU 高
问题:Java 进程 CPU 100% 怎么办?
回答:
top -H -p <pid>
printf "%x\n" <tid>
jcmd <pid> Thread.print
然后:
- 定位高 CPU 线程;
- 判断业务、GC、JIT 或其他 JVM 线程;
- 用 JFR 或 Arthas 定位方法;
- 结合流量和输入特征;
- 修复后验证。
加分点:说明容器 CPU throttling 和 JVM 感知处理器数。
30.16 线程池参数
问题:线程池核心参数有哪些?
回答:
corePoolSize
maximumPoolSize
keepAliveTime
workQueue
threadFactory
rejectedExecutionHandler
提交流程:
core 未满 -> 创建核心线程
core 满 -> 入队
队列满且 max 未满 -> 创建非核心线程
队列满且 max 满 -> 拒绝
注意无界队列会让 maximumPoolSize 失效。生产要显式命名线程、设置有界队列、监控队列和拒绝数。
30.17 类冲突
问题:ClassNotFoundException 和 NoClassDefFoundError 有什么区别?
| 类型 | 时机 | 常见原因 |
|---|---|---|
| ClassNotFoundException | 显式加载时抛异常 | 类路径缺失 |
| NoClassDefFoundError | 编译时存在,运行时初始化失败 | 依赖缺失、版本冲突、静态块失败 |
排查:
- 看完整异常栈;
- 确认 jar 是否在 classpath;
- 检查同名类多个版本;
- 使用
javap反汇编; - 检查类加载器和模块可见性; 6| 查看-parent 子类缺失父类的情况。
30.18 高频反问
面试官可能追问:
- 你实际用过哪个收集器;
- 你们服务的堆多大、延迟目标多少;
- 调优前后指标是什么;
- dump 有多大,怎么分析;
- 为什么不用另一个收集器;
- 容器内存如何规划;
- 如何确认参数生效;
- 如何避免 GC 调优只优化了平均值。
回答原则:
先给结论
再讲证据
说明版本和边界
承认不确定性
展示验证过程
本章小结
JVM 面试要能从概念、实现、版本差异、生产排查四个层面回答。核心区域、类加载、GC、JMM、锁、线程池、OOM 和 CPU 排查是高频主线。回答时引用实际指标、日志、dump 和压测过程,比背诵名词更能体现能力。
思考题
- 为什么内存区域和 Java 内存模型不能混为一谈?
- 如何向面试官描述一次完整的 GC 调优?
- volatile 和 synchronized 分别适合什么场景?
- 你如何回答“G1 和 ZGC 怎么选”?
- 哪些 JVM 面试答案必须补充 JDK 版本?