JVMNotes

第 30 章:面试题精讲

zjc 于 2026-01-30 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章把 JVM 面试中的高频问题整理成可讲清、可追问、可落地的答案。面试的重点不是背诵结论,而是展示边界意识和排查能力。

30.1 JVM 内存区域

问题:JVM 运行时数据区有哪些?

回答要点:

  1. 堆:对象和数组,线程共享;
  2. 方法区:类元数据、运行时常量池,HotSpot 现为 Metaspace 实现;
  3. 虚拟机栈:栈帧、局部变量表、操作数栈;
  4. 本地方法栈:Native 方法;
  5. 程序计数器:线程私有,记录当前执行字节码位置。

加分点:

JDK 8 移除永久代,Metaspace 使用本地内存;
虚拟机栈可能 StackOverflowError;
堆和 Metaspace 可能 OOM;
直接内存不属于 JVM 规范定义的堆,但会影响进程总内存。

30.2 对象创建过程

问题:new 一个对象时发生什么?

简化流程:

1. 类加载检查
2. 分配内存
3. 初始化零值
4. 设置对象头
5. 执行 <init> 构造逻辑

追问点:

  1. 指针碰撞还是空闲列表取决于堆是否规整;
  2. 分配可能使用 TLAB;
  3. 对象头包含 Mark Word 和类型指针;
  4. 大对象可能进入老年代或特殊 Region。

30.3 对象内存布局

问题:对象在堆里如何存储?

结构:

Object Header
  Mark Word
  Klass Pointer
Instance Data
  字段,可能重排
Padding
  对齐到 8 字节

压缩指针:

  1. 开启压缩 OOP 时,Klass Pointer 可能为 4 字节;
  2. 堆超过一定范围后可能失效;
  3. 对齐方式可能调整;
  4. 具体行为与 JVM 版本和参数有关。

30.4 类加载过程

问题:类加载有几个阶段?

回答:

加载 -> 验证 -> 准备 -> 解析 -> 初始化
阶段 关键动作
加载 读取字节流生成 Class 对象
验证 格式、元数据、字节码、符号引用
准备 静态变量分配并设零值
解析 符号引用转直接引用
初始化 执行 <clinit>

追问:静态变量在准备阶段是零值,static final int x = 1 这类编译期常量可能在准备阶段直接赋值。

30.5 双亲委派

问题:什么是双亲委派?

流程:

ClassLoader.loadClass
  -> 先委托 parent
  -> parent 找不到再由当前 loader findClass

作用:

  1. 避免核心类被重复加载;
  2. 防止用户替换核心 API;
  3. 保证类的一致命名空间。

破坏场景:

  1. SPI 需要父加载器使用子加载器;
  2. Web 容器隔离应用类;
  3. OSGi 模块化;
  4. 热部署和动态代理。

“破坏”不一定是贬义,而是为满足隔离和可见性需求做的设计取舍。

30.6 判断对象存活

问题:JVM 如何判断对象可以回收?

回答:

  1. 主流 HotSpot 使用可达性分析;
  2. 从 GC Roots 出发搜索;
  3. 不可达对象可回收;
  4. finalize 已废弃,不应作为资源释放方案;
  5. 引用类型影响回收时机。

常见 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 有什么优势?

回答:

  1. G1 是 Region 化布局;
  2. 可预测停顿目标;
  3. 标记整理降低碎片;
  4. Mixed GC 逐步回收 Old;
  5. CMS 在 JDK 14 被移除。

CMS 的历史问题:

并发标记清除产生碎片
concurrent mode failure 退级 Serial Old
Remark 阶段成本
Remembered Set 维护成本

面试中要说明自己使用的是哪个 JDK,避免把 JDK 8 经验泛化。

30.10 ZGC 特点

问题:ZGC 为什么停顿低?

回答:

  1. 标记、搬迁大部分并发执行;
  2. 着色指针记录对象状态;
  3. 读屏障帮助修正引用;
  4. 停顿阶段工作量小;
  5. JDK 21 引入分代模式。

代价:

  1. 内存开销更高;
  2. 读屏障影响吞吐;
  3. 并发阶段消耗 CPU;
  4. 堆太满时可能 Allocation Stall。

30.11 volatile

问题:volatile 有什么作用?

回答:

  1. 可见性:写入后其他线程能读到新值;
  2. 有序性:禁止特定指令重排;
  3. 不保证原子性。

示例:

private volatile boolean running = true;

public void stop() {
    running = false;
}

适合状态标志。volatile int count; count++ 仍不是线程安全的,应使用 AtomicInteger 或锁。

30.12 synchronized 锁升级

问题:synchronized 有哪些锁状态?

简化路径:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

要点:

  1. JDK 15 起默认废弃偏向锁;
  2. 锁状态存储在对象头 Mark Word;
  3. 竞争程度影响升级;
  4. 锁不一定是“只能升级不能降级”的简单过程;
  5. JIT 可能锁消除或锁粗化。

回答时补充版本差异,比背固定路径更专业。

30.13 Java 内存模型

问题:JMM 解决什么问题?

回答:

  1. 定义共享变量的可见性;
  2. 定义指令重排的边界;
  3. 定义 happens-before 规则;
  4. 为 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. 修复后压测验证

加分:

  1. 区分堆、Metaspace、线程、直接内存;
  2. 区分 Java OOM 和 OOMKilled;
  3. 说明 dump 会 STW;
  4. 说明自动 dump 参数;
  5. 给出真实案例结构。

30.15 排查 CPU 高

问题:Java 进程 CPU 100% 怎么办?

回答:

top -H -p <pid>
printf "%x\n" <tid>
jcmd <pid> Thread.print

然后:

  1. 定位高 CPU 线程;
  2. 判断业务、GC、JIT 或其他 JVM 线程;
  3. 用 JFR 或 Arthas 定位方法;
  4. 结合流量和输入特征;
  5. 修复后验证。

加分点:说明容器 CPU throttling 和 JVM 感知处理器数。

30.16 线程池参数

问题:线程池核心参数有哪些?

回答:

corePoolSize
maximumPoolSize
keepAliveTime
workQueue
threadFactory
rejectedExecutionHandler

提交流程:

core 未满 -> 创建核心线程
core 满 -> 入队
队列满且 max 未满 -> 创建非核心线程
队列满且 max 满 -> 拒绝

注意无界队列会让 maximumPoolSize 失效。生产要显式命名线程、设置有界队列、监控队列和拒绝数。

30.17 类冲突

问题:ClassNotFoundException 和 NoClassDefFoundError 有什么区别?

类型 时机 常见原因
ClassNotFoundException 显式加载时抛异常 类路径缺失
NoClassDefFoundError 编译时存在,运行时初始化失败 依赖缺失、版本冲突、静态块失败

排查:

  1. 看完整异常栈;
  2. 确认 jar 是否在 classpath;
  3. 检查同名类多个版本;
  4. 使用 javap 反汇编;
  5. 检查类加载器和模块可见性; 6| 查看-parent 子类缺失父类的情况。

30.18 高频反问

面试官可能追问:

  1. 你实际用过哪个收集器;
  2. 你们服务的堆多大、延迟目标多少;
  3. 调优前后指标是什么;
  4. dump 有多大,怎么分析;
  5. 为什么不用另一个收集器;
  6. 容器内存如何规划;
  7. 如何确认参数生效;
  8. 如何避免 GC 调优只优化了平均值。

回答原则:

先给结论
再讲证据
说明版本和边界
承认不确定性
展示验证过程

本章小结

JVM 面试要能从概念、实现、版本差异、生产排查四个层面回答。核心区域、类加载、GC、JMM、锁、线程池、OOM 和 CPU 排查是高频主线。回答时引用实际指标、日志、dump 和压测过程,比背诵名词更能体现能力。

思考题

  1. 为什么内存区域和 Java 内存模型不能混为一谈?
  2. 如何向面试官描述一次完整的 GC 调优?
  3. volatile 和 synchronized 分别适合什么场景?
  4. 你如何回答“G1 和 ZGC 怎么选”?
  5. 哪些 JVM 面试答案必须补充 JDK 版本?