JVM面试要点:从内存结构、类加载、GC到JDK 8/17/21演进的系统梳理
前言
JVM 是 Java 面试里的核心专题。它连接了语言特性、运行时性能、线上排查和版本升级,常见问题包括:JVM 内存结构是什么,堆和栈有什么区别,对象怎么创建,类加载过程是什么,双亲委派有什么用,哪些对象可以作为 GC Roots,Minor GC 和 Full GC 有什么区别,G1 为什么适合大堆,JDK 8、17、21 在 JVM 层面有哪些变化。
如果只背“堆放对象、栈放局部变量”,回答通常不够。更好的方式是把 JVM 理解成一条完整链路:
1 | Java 源码 -> 字节码 -> 类加载 -> 运行时内存 -> 对象分配 -> 垃圾回收 -> JIT 优化 -> 线上监控与调优 |
本文按面试高频模块梳理 JVM 要点,并单独总结 JDK 8、JDK 17、JDK 21 的关键变化。
JVM 整体结构
JVM 的核心职责是加载字节码、管理运行时内存、执行程序、回收无用对象,并通过即时编译优化热点代码。
flowchart TD
A[Java 源码] --> B[javac 编译为 class 字节码]
B --> C[类加载子系统]
C --> D[运行时数据区]
D --> E[执行引擎]
E --> F[解释执行和 JIT 编译]
D --> G[垃圾回收器]
G --> D
E --> H[本地方法接口 JNI]
面试中可以把 JVM 拆成几块:
- 类加载子系统:加载、验证、准备、解析、初始化。
- 运行时数据区:堆、方法区、虚拟机栈、本地方法栈、程序计数器。
- 执行引擎:解释器、JIT 编译器、垃圾回收器。
- 本地方法接口:调用 Native 方法。
运行时内存结构
JVM 运行时数据区通常分为线程共享和线程私有两类。
| 区域 | 是否线程共享 | 主要内容 | 常见问题 |
|---|---|---|---|
| 堆 | 是 | 对象实例、数组 | OutOfMemoryError: Java heap space |
| 方法区/元空间 | 是 | 类元数据、常量、静态变量相关信息 | OutOfMemoryError: Metaspace |
| 虚拟机栈 | 否 | 栈帧、局部变量表、操作数栈 | StackOverflowError |
| 本地方法栈 | 否 | Native 方法调用栈 | Native 调用异常 |
| 程序计数器 | 否 | 当前线程执行位置 | 一般不会 OOM |
flowchart LR
A[JVM 运行时数据区] --> B[线程共享]
A --> C[线程私有]
B --> D[堆]
B --> E[方法区/元空间]
C --> F[虚拟机栈]
C --> G[本地方法栈]
C --> H[程序计数器]
面试回答要点:
- 堆是 GC 管理的主要区域。
- 栈跟线程生命周期绑定,每个方法调用都会创建栈帧。
- 方法区在 JDK 8 之后由元空间实现,元空间使用本地内存。
- 程序计数器用于记录当前线程执行到哪里,是线程切换后恢复执行位置的依据。
堆、栈和方法区
堆
堆用于存放对象实例和数组,是垃圾回收重点区域。常见分代模型包括新生代和老年代。
新生代一般又分为:
- Eden 区。
- Survivor From 区。
- Survivor To 区。
对象通常优先在 Eden 分配。一次 Minor GC 后,仍然存活的对象会进入 Survivor 区,多次存活或大对象可能进入老年代。
虚拟机栈
每个线程都有自己的虚拟机栈。每次方法调用会创建一个栈帧,栈帧中包含:
- 局部变量表。
- 操作数栈。
- 动态链接。
- 方法返回地址。
常见追问:
- 栈溢出通常来自递归过深或调用链过长。
- 栈不是 GC 的主要管理区域,因为栈帧随方法调用结束自然出栈。
- 局部变量表里的对象引用可以作为 GC Roots。
方法区和元空间
方法区是 JVM 规范中的逻辑区域,存放类元数据、运行时常量池、方法信息等。HotSpot 在 JDK 8 之前使用永久代实现方法区,JDK 8 之后移除了永久代,改为元空间。
关键区别:
- 永久代在 JVM 堆内或受 JVM 参数强约束,容易遇到
PermGen space。 - 元空间使用本地内存,默认上限受机器内存影响。
- 元空间仍可能 OOM,常见原因是动态生成类过多、类加载器泄漏。
对象创建过程
对象创建通常包括以下步骤:
- 类加载检查:确认类是否已经加载、解析和初始化。
- 分配内存:在堆中为对象分配空间。
- 初始化零值:对象字段先置为默认值。
- 设置对象头:写入 Mark Word、类型指针等信息。
- 执行构造方法:执行
<init>,完成业务初始化。
flowchart TD
A[new 对象] --> B{类是否已加载}
B -->|否| C[触发类加载]
B -->|是| D[堆上分配内存]
C --> D
D --> E[字段初始化为零值]
E --> F[设置对象头]
F --> G[执行构造方法]
G --> H[返回对象引用]
内存分配方式:
- 指针碰撞:堆内存规整时使用。
- 空闲列表:堆内存不规整时使用。
- TLAB:线程本地分配缓冲,减少多线程分配对象时的竞争。
对象内存布局
HotSpot 中普通对象大致由三部分组成:
- 对象头:Mark Word、类型指针,数组对象还包含数组长度。
- 实例数据:对象字段内容。
- 对齐填充:保证对象大小按一定字节对齐。
Mark Word 里会存放对象哈希码、GC 年龄、锁状态等信息。锁升级、偏向锁、轻量级锁、重量级锁等问题都和对象头有关。
类加载过程
类加载过程可以分为五个阶段:
| 阶段 | 作用 |
|---|---|
| 加载 | 读取 class 字节流,生成 Class 对象 |
| 验证 | 校验字节码是否合法、安全 |
| 准备 | 为静态变量分配内存并设置默认值 |
| 解析 | 将符号引用转换为直接引用 |
| 初始化 | 执行类构造器 <clinit> |
flowchart LR
A[加载] --> B[验证]
B --> C[准备]
C --> D[解析]
D --> E[初始化]
注意准备阶段和初始化阶段的区别:
1 | public static int value = 10; |
准备阶段 value 是默认值 0,初始化阶段才赋值为 10。
双亲委派模型
双亲委派指类加载器收到类加载请求时,先委托父加载器加载,父加载器无法加载时,子加载器才尝试加载。
主要目的:
- 避免核心类被重复加载。
- 防止用户自定义类替换 JDK 核心类。
- 保证类加载体系的稳定性。
常见类加载器:
- Bootstrap ClassLoader:加载 JDK 核心类。
- Platform ClassLoader:加载平台类。
- Application ClassLoader:加载应用 classpath 下的类。
- 自定义 ClassLoader:框架、插件、热部署场景使用。
面试追问:能不能打破双亲委派?可以,例如 SPI、OSGi、Tomcat 多 Web 应用隔离、一些热部署框架都会在特定场景下调整加载逻辑。
垃圾回收基础
垃圾回收要解决三个问题:
- 哪些对象已经没用了。
- 什么时候回收。
- 怎么回收并减少停顿。
判断对象是否可回收主要有两种思路:
- 引用计数法:简单但难解决循环引用。
- 可达性分析:从 GC Roots 出发,无法到达的对象可以回收。
Java 主流 JVM 使用可达性分析。
GC Roots
常见 GC Roots 包括:
- 虚拟机栈中引用的对象。
- 本地方法栈中 JNI 引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 被
synchronized持有的对象。 - JVM 内部引用,例如系统类加载器、基础异常对象等。
面试回答时可以举例:
1 | public class Demo { |
staticObj 可能通过类静态属性成为根可达对象,localObj 在方法执行期间可能通过栈帧局部变量表成为根可达对象。
引用类型
Java 引用分为四类:
| 引用类型 | 回收特点 | 典型用途 |
|---|---|---|
| 强引用 | 只要可达就不回收 | 普通对象引用 |
| 软引用 | 内存不足时可能回收 | 缓存 |
| 弱引用 | 下一次 GC 时回收 | WeakHashMap、ThreadLocal key |
| 虚引用 | 不影响生命周期,用于回收通知 | 堆外内存清理 |
ThreadLocal 内存泄漏经常和弱引用一起考:ThreadLocalMap 的 key 是弱引用,但 value 是强引用。线程长期存活时,如果不 remove,value 可能无法及时释放。
垃圾回收算法
常见 GC 算法:
- 标记-清除:先标记可回收对象,再清除;问题是会产生内存碎片。
- 标记-复制:把存活对象复制到另一块区域;适合新生代。
- 标记-整理:标记后移动存活对象,整理出连续空间;适合老年代。
- 分代收集:根据对象生命周期不同,选择不同算法。
flowchart TD
A[对象生命周期差异] --> B[新生代对象朝生夕死]
A --> C[老年代对象存活更久]
B --> D[复制算法]
C --> E[标记-清除或标记-整理]
常见垃圾收集器
| 收集器 | 主要特点 | 常见场景 |
|---|---|---|
| Serial | 单线程,简单 | 客户端、小内存 |
| Parallel | 吞吐优先 | 批处理、后台任务 |
| CMS | 低停顿,基于标记清除 | JDK 8 常见老年代方案,后续被移除 |
| G1 | 分区、可预测停顿 | JDK 9 后默认,服务端常用 |
| ZGC | 超低停顿,可扩展大堆 | 大内存、低延迟 |
| Shenandoah | 超低停顿 | 低延迟场景 |
G1
G1 把堆划分为多个 Region,不再严格按连续的新生代、老年代物理区域管理。它会优先回收收益最高的 Region,所以叫 Garbage First。
G1 的特点:
- 面向服务端和大堆。
- 支持设置停顿时间目标。
- 使用 Region 管理堆。
- 通过 Remembered Set 处理跨 Region 引用。
- 适合对延迟和吞吐都有要求的通用服务。
ZGC
ZGC 目标是低停顿。它使用染色指针、读屏障、并发整理等机制,把大部分 GC 工作和应用线程并发执行。
面试中不需要展开所有实现细节,但要知道它适合大内存、低延迟应用,停顿时间通常不是随堆大小线性增长。
Minor GC、Major GC 和 Full GC
常见概念:
- Minor GC:新生代 GC。
- Major GC:老年代 GC,不同资料和日志语境中定义可能不完全一致。
- Full GC:通常指覆盖整个堆和方法区相关区域的完整回收,停顿更重。
Full GC 常见原因:
- 老年代空间不足。
- 元空间不足。
- 显式调用
System.gc()。 - 大对象分配失败。
- 晋升失败或空间担保失败。
线上排查时不要只看 GC 次数,还要看停顿时间、回收前后内存变化、对象晋升速度和业务 RT 的对应关系。
JVM 参数
常见内存参数:
1 | -Xms 初始堆大小 |
常见 GC 参数:
1 | -XX:+UseG1GC |
JDK 9 之后统一日志常用:
1 | -Xlog:gc*:file=gc.log:time,uptime,level,tags |
JDK 8 常见 GC 日志参数:
1 | -XX:+PrintGCDetails |
常见线上排查
CPU 高
排查路径:
- 找到 Java 进程。
- 找到高 CPU 线程。
- 把线程 ID 转成十六进制。
- 用
jstack查对应 nid。 - 判断是死循环、频繁 GC、锁竞争、正则回溯还是计算热点。
内存持续上涨
排查路径:
- 看堆使用趋势和 Full GC 后是否下降。
- 生成 heap dump。
- 用 MAT、VisualVM、JProfiler 等工具分析大对象和引用链。
- 关注集合缓存、静态 Map、ThreadLocal、连接池、监听器、类加载器泄漏。
- 区分堆内内存、元空间、直接内存和本地内存。
频繁 Full GC
常见原因:
- 堆设置过小。
- 老年代增长过快。
- 大对象过多。
- 对象生命周期异常。
- 元空间不足。
- 代码里显式调用
System.gc()。
线程卡死
排查路径:
jstack查看线程状态。- 关注
BLOCKED、WAITING、TIMED_WAITING。 - 查是否有 deadlock。
- 看锁持有者正在执行什么。
- 结合慢 SQL、远程调用、IO、线程池队列一起分析。
JDK 8 JVM 要点
JDK 8 是很多存量系统的长期基线,面试中经常围绕以下变化展开:
永久代移除
JDK 8 移除了永久代,使用元空间保存类元数据。
常见影响:
PermGen space相关问题变成Metaspace问题。- 元空间使用本地内存,不再直接受堆大小限制。
- 动态生成类、类加载器泄漏仍然可能导致元空间 OOM。
默认和常用 GC
JDK 8 服务端常见组合包括 Parallel GC、CMS、G1。很多老系统使用 CMS 追求低停顿,但 CMS 有浮动垃圾、内存碎片、并发失败等问题。
Lambda 和 invokedynamic
JDK 8 引入 Lambda。Lambda 背后大量使用 invokedynamic,把一部分动态调用链接工作推迟到运行时完成。面试中通常不要求手写字节码,但要知道 Lambda 不是简单匿名内部类的语法糖。
日期时间和函数式编程
这部分更多是 Java 语言和类库特性,但面试里经常和 JDK 8 一起问:
- Lambda。
- Stream API。
- Optional。
- 新日期时间 API。
- 接口默认方法。
JDK 17 JVM 要点
JDK 17 是 LTS 版本,也是很多企业从 JDK 8 升级的重要目标。
G1 成为主流默认选择
从 JDK 9 开始,G1 成为默认垃圾收集器。到 JDK 17,G1 已经是服务端应用非常常见的默认选择。
面试回答可以强调:
- JDK 17 默认更偏向 G1。
- G1 通过 Region 管理堆,目标是兼顾吞吐和停顿。
- 升级时要重新观察 GC 日志,不能照搬 JDK 8 的 CMS 参数。
CMS 被移除
CMS 在 JDK 9 被废弃,JDK 14 被移除。因此 JDK 17 不能继续使用 CMS。老系统升级时,如果还配置了 CMS 参数,需要改成 G1、ZGC 或其他可用收集器。
ZGC 可用于生产
ZGC 在 JDK 15 转正,JDK 17 中已经是可用于生产的低延迟 GC 方案。它适合大堆和低停顿场景,但具体是否使用要结合吞吐、延迟、内存成本和业务特征压测。
强封装和模块化影响
JDK 9 引入模块系统后,JDK 17 对 JDK 内部 API 的访问限制更严格。升级时常见问题包括:
- 反射访问 JDK 内部类失败。
- 需要添加
--add-opens或升级依赖。 - 老版本字节码增强、序列化、代理框架不兼容。
JDK 21 JVM 要点
JDK 21 是 LTS 版本,面试中常和虚拟线程、ZGC 分代、结构化并发等一起问。
虚拟线程
虚拟线程是 JDK 21 的正式特性。它不是简单地创建更多 OS 线程,而是在 JVM 层面提供轻量级线程,由 JVM 调度到平台线程上执行。
适合场景:
- 大量阻塞式 IO。
- HTTP 调用、数据库访问、RPC 调用较多的服务。
- 希望保留同步编程模型,同时提升并发承载能力。
不适合场景:
- CPU 密集型任务。
- 依赖固定线程数做资源隔离的老代码。
- 大量使用
ThreadLocal且没有治理的场景。
面试要点:
- 虚拟线程提升的是阻塞 IO 场景下的并发效率,不是让 CPU 计算变快。
- 使用虚拟线程后仍然需要控制数据库连接池、下游限流和资源上限。
synchronized、本地方法、某些阻塞操作可能导致 pinning,需要关注实际运行表现。
分代 ZGC
JDK 21 引入分代 ZGC。相比非分代 ZGC,分代 ZGC 更符合大多数 Java 对象“朝生夕死”的特点,在低停顿目标下进一步改善回收效率。
面试可以这样回答:
- ZGC 追求超低停顿。
- 分代 ZGC 增加了年轻代和老年代的区分。
- 它适合大内存、低延迟服务,但要通过压测确认吞吐和资源成本。
其他常见 JDK 21 相关点
JDK 21 还包含一些语言和并发层面的正式或预览能力,面试中经常和 JVM 一起聊:
- 虚拟线程正式可用。
- Record Patterns、Pattern Matching 等语法演进提升表达能力。
- Sequenced Collections 补齐集合顺序访问抽象。
- String Templates 在预览阶段,生产使用要谨慎。
回答时要区分“JVM 运行时能力”和“Java 语言/API 特性”,不要把所有 JDK 新特性都说成 JVM 特性。
JDK 8、17、21 对比
| 版本 | 定位 | JVM 相关重点 | 面试关注点 |
|---|---|---|---|
| JDK 8 | 存量系统基线 | 永久代移除、元空间、Lambda 与 invokedynamic、CMS/G1 常见 | PermGen 到 Metaspace、CMS 问题、JDK 8 新特性 |
| JDK 17 | 企业升级常见 LTS | G1 主流默认、CMS 不可用、ZGC 可生产、强封装 | JDK 8 升级兼容性、GC 参数迁移、反射访问限制 |
| JDK 21 | 新 LTS | 虚拟线程、分代 ZGC、并发模型演进 | 虚拟线程适用边界、低延迟 GC、资源隔离 |
timeline
title JDK 8/17/21 JVM 演进主线
2014 : JDK 8 : 永久代移除 : 元空间 : Lambda 和 invokedynamic
2021 : JDK 17 : G1 主流 : CMS 已移除 : ZGC 可生产 : 强封装
2023 : JDK 21 : 虚拟线程正式 : 分代 ZGC : 新一代并发模型
高频面试题
JVM 内存结构有哪些?
主要包括堆、方法区或元空间、虚拟机栈、本地方法栈、程序计数器。堆和方法区是线程共享的,栈、本地方法栈、程序计数器是线程私有的。
堆和栈有什么区别?
堆存放对象实例和数组,是 GC 重点管理区域;栈存放方法调用产生的栈帧,包括局部变量表、操作数栈等,随线程和方法调用变化。
JDK 8 为什么移除永久代?
永久代大小难以合理设置,容易出现 PermGen space。JDK 8 改用元空间存放类元数据,元空间使用本地内存,但类加载器泄漏或动态生成类过多仍会导致 OOM。
类加载过程是什么?
加载、验证、准备、解析、初始化。准备阶段给静态变量赋默认值,初始化阶段才执行静态变量赋值和静态代码块。
双亲委派有什么用?
保证核心类优先由父加载器加载,避免核心类被重复加载或被用户类篡改,维持 Java 类加载体系稳定。
哪些对象可以作为 GC Roots?
栈中引用的对象、静态属性引用的对象、常量引用的对象、JNI 引用的对象、被锁持有的对象、JVM 内部引用等。
G1 为什么适合大堆?
G1 使用 Region 划分堆,可以按回收收益选择 Region,并通过停顿目标控制回收节奏,适合对停顿时间有要求的服务端应用。
JDK 17 升级要注意什么?
注意 CMS 参数不可用、G1 行为变化、强封装导致反射访问受限、老依赖兼容性、GC 日志参数变化,以及需要重新压测和观察内存、GC、RT。
JDK 21 虚拟线程能解决什么问题?
主要解决大量阻塞 IO 场景下平台线程成本高的问题。它能提升并发承载能力,但不能让 CPU 密集型计算变快,也不能替代连接池、限流和资源隔离。
总结
JVM 面试可以围绕五条主线回答:
- 内存怎么组织:堆、栈、元空间、程序计数器。
- 类怎么进入 JVM:类加载过程、双亲委派、类初始化。
- 对象怎么管理:对象创建、内存布局、引用类型、可达性分析。
- GC 怎么工作:GC Roots、分代回收、G1、ZGC、Full GC 排查。
- 版本怎么演进:JDK 8 的元空间,JDK 17 的 G1、强封装和 CMS 移除,JDK 21 的虚拟线程和分代 ZGC。
把这些点串起来,回答 JVM 问题时就不只是背参数,而是能说明 Java 程序从字节码到运行时、从对象分配到垃圾回收、从版本升级到线上调优的完整机制。


