JVM 基础高频面试题(含口述版回答)
1. 讲讲 JVM 的内存结构(运行时数据区)
口述版:
JVM 运行时数据区主要分为以下几块:
线程私有:
- 程序计数器:记录当前线程执行到的字节码行号,线程切换后能恢复。唯一不会 OOM 的区域。
- 虚拟机栈:每个方法执行时创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等。栈溢出抛
StackOverflowError。 - 本地方法栈:为 Native 方法服务,和虚拟机栈类似。
线程共享:
- 堆(Heap):存放对象实例和数组,是 GC 的主要区域。分为新生代和老年代。
- 方法区(Metaspace):存储类信息、常量、静态变量、JIT 编译后的代码等。JDK 8 后用元空间(本地内存)替代永久代。
另外还有直接内存(堆外内存),NIO 会用到,不在运行时数据区里,但也会导致 OOM。
2. 堆和栈的区别?
口述版:
| 对比点 | 堆(Heap) | 栈(Stack) |
|---|---|---|
| 存放内容 | 对象实例、数组 | 局部变量、方法调用信息 |
| 共享性 | 线程共享 | 线程私有 |
| 大小 | 较大,可动态调整 | 较小,固定或有限增长 |
| 异常 | OutOfMemoryError | StackOverflowError |
| 管理方式 | GC 自动回收 | 方法结束自动弹出栈帧 |
| 速度 | 相对慢 | 很快 |
简单记:对象在堆,方法调用在栈。栈的生命周期随方法,堆的生命周期由 GC 决定。
3. 对象的创建过程是怎样的?
口述版:
以 new 一个对象为例,大致流程:
- 类加载检查:检查类是否已加载,没有则先加载。
- 分配内存:在堆中划分一块空间(指针碰撞或空闲列表)。
- 初始化零值:把对象的实例变量设为默认值(0、null 等)。
- 设置对象头:写入 Mark Word、类型指针、数组长度等信息。
- 执行
<init>方法:调用构造方法,进行真正的初始化。
并发分配内存时,用 CAS + 失败重试,或者用 TLAB(线程本地分配缓冲)减少竞争。
4. 什么是 GC Roots?哪些对象可以作为 GC Roots?
口述版:
GC Roots 是垃圾回收时的起始点,通过可达性分析判断对象是否存活。从 GC Roots 出发,无法到达的对象就可以被回收。
常见的 GC Roots 包括:
- 虚拟机栈中引用的对象(局部变量)
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- 被同步锁持有的对象
- JVM 内部的一些特殊对象(如系统类加载器)
5. 常见的垃圾回收算法有哪些?
口述版:
- 标记-清除(Mark-Sweep):标记存活对象,再清除未标记的。缺点是产生内存碎片。
- 标记-复制(Mark-Copy):把存活对象复制到另一块空间,然后清空原空间。适合新生代(存活对象少),缺点是浪费一半空间。
- 标记-整理(Mark-Compact):标记后把存活对象向一端移动,清理边界外的内存。适合老年代,无碎片但移动成本高。
- 分代收集:根据对象生命周期把堆分成新生代和老年代,采用不同算法。这是现代 JVM 的基础思想。
新生代一般用复制算法,老年代用标记-清除或标记-整理。
6. 讲讲新生代和老年代,以及常见的垃圾收集器
口述版:
堆分代:
- 新生代:新创建的对象优先分配在这里,又分为 Eden + 两个 Survivor(From/To)。大多数对象朝生夕死。
- 老年代:经历多次 Minor GC 还存活的对象会进入老年代,或者大对象直接进入。
常见收集器:
| 收集器 | 工作区域 | 特点 | 算法 |
|---|---|---|---|
| Serial | 新生代 | 单线程,STW,适合客户端 | 复制 |
| ParNew | 新生代 | Serial 的多线程版本 | 复制 |
| Parallel Scavenge | 新生代 | 注重吞吐量 | 复制 |
| Serial Old | 老年代 | 单线程 | 标记-整理 |
| Parallel Old | 老年代 | 注重吞吐量 | 标记-整理 |
| CMS | 老年代 | 低延迟,标记-清除,有碎片 | 标记-清除 |
| G1 | 整堆 | 分区(Region),可预测停顿 | 复制 + 整理 |
| ZGC / Shenandoah | 整堆 | 超低延迟(毫秒级) | 染色指针等 |
现在服务端比较主流的是 G1(JDK 9 默认),超低延迟场景会考虑 ZGC。
7. Minor GC、Major GC、Full GC 的区别?
口述版:
- Minor GC(Young GC):只清理新生代。很频繁,速度相对快。
- Major GC:通常指清理老年代,有时也和 Full GC 混用。
- Full GC:清理整个堆(新生代 + 老年代)以及方法区。停顿时间长,应尽量减少。
触发 Full GC 的常见原因:
- 老年代空间不足
- 方法区(Metaspace)空间不足
- 调用
System.gc()(建议别主动调) - 晋升失败、并发模式失败(CMS)等
8. 类加载过程是怎样的?什么是双亲委派模型?
口述版:
类加载过程分三个大步骤:
- 加载(Loading):通过类名获取字节流,转换成方法区的运行时结构,在堆中生成 Class 对象。
- 链接(Linking):
- 验证:确保字节码安全
- 准备:为静态变量分配内存并设零值
- 解析:把符号引用转为直接引用
- 初始化(Initialization):执行类构造器
<clinit>,给静态变量赋真正的值,执行静态代码块。
双亲委派模型: 类加载器收到加载请求时,先委托给父加载器,只有父加载器找不到才自己加载。
好处:
- 避免类的重复加载
- 保证核心类(如 String)不被篡改,安全
类加载器层次: Bootstrap → Extension/Platform → Application → 自定义
9. 强引用、软引用、弱引用、虚引用的区别?
口述版:
| 引用类型 | 回收时机 | 用途示例 |
|---|---|---|
| 强引用 | 永不回收(只要还在引用) | 普通对象 Object obj = new Object() |
| 软引用 | 内存不足时回收 | 缓存(SoftReference) |
| 弱引用 | 下次 GC 就回收 | ThreadLocal、WeakHashMap |
| 虚引用 | 随时可能回收,无法通过它获取对象 | 跟踪对象被回收的状态(配合引用队列) |
实际开发中,软引用常用于对内存敏感的缓存,弱引用常用于防止内存泄漏(如 ThreadLocal 的 key)。
10. 什么是内存泄漏?和内存溢出有什么区别?
口述版:
- 内存溢出(OOM):程序申请的内存超过了 JVM 能提供的最大内存,直接抛
OutOfMemoryError。 - 内存泄漏(Memory Leak):对象已经不再被使用,但由于引用关系还存在,GC 无法回收,导致可用内存逐渐减少。
泄漏最终往往会导致溢出。
常见泄漏场景:
- 静态集合一直持有对象引用
- 监听器、回调没被移除
- ThreadLocal 使用完没 remove
- 连接、流等资源没关闭
- 内部类持有外部类引用
11. 常见的 JVM 调优参数有哪些?
口述版:
基础必记:
-Xms:初始堆大小-Xmx:最大堆大小(建议和 Xms 设一样,避免动态扩容)-Xmn:新生代大小-XX:SurvivorRatio:Eden 和 Survivor 的比例(默认 8)-XX:MetaspaceSize/-XX:MaxMetaspaceSize:元空间大小-XX:+UseG1GC:使用 G1 收集器-XX:MaxGCPauseMillis:G1 期望的最大停顿时间-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动生成堆转储-XX:HeapDumpPath:堆转储文件路径
排查问题时常用:
-XX:+PrintGCDetails(老版本)- 或统一用
-Xlog:gc*(JDK 9+)
调优原则:先监控、定位问题,再有针对性地改参数,不要一上来就乱调。
12. 如何排查 OOM 和内存问题?
口述版:
一般步骤:
- 加上
-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动 dump。 - 用 jmap 或 jcmd 手动生成堆转储。
- 用 MAT(Memory Analyzer) 或 VisualVM 分析 dump 文件,找占用内存最大的对象和引用链。
- 结合 jstat 看 GC 情况,jstack 看线程状态。
- 如果是堆外内存问题,可能要看 Direct Memory 或使用 Native Memory Tracking。
线上还可以用 Arthas 做动态诊断。
13. JIT 编译器是什么?有什么作用?
口述版:
JIT(Just-In-Time)即时编译器。
Java 先把源码编译成字节码,再由解释器执行。为了提高性能,JVM 会把热点代码(执行频繁的方法或循环)编译成本地机器码,这就是 JIT。
主要有两个编译器:
- C1(Client):编译速度快,优化程度低
- C2(Server):编译慢,但优化更激进
分层编译(Tiered Compilation)会结合两者的优点。逃逸分析、锁消除、标量替换等优化都是 JIT 做的。
14. 对象头里有什么?Mark Word 是干什么的?
口述版:
对象在内存中分为三部分:
- 对象头
- 实例数据
- 对齐填充
对象头又包括:
- Mark Word:存储哈希码、GC 分代年龄、锁状态、偏向线程 ID 等信息。锁升级(偏向锁 → 轻量级锁 → 重量级锁)主要改的就是这里的状态位。
- 类型指针(Klass Pointer):指向类的元数据。
- 如果是数组,还有数组长度。
了解 Mark Word 有助于理解 synchronized 的锁升级过程。
15. 双亲委派模型会被破坏吗?举例说明
口述版:
会被破坏,常见场景:
- SPI 机制(如 JDBC):核心类(DriverManager)需要加载厂商实现的驱动类,由启动类加载器委托上下文类加载器去加载。
- 热部署、热加载:像 Tomcat 的 WebAppClassLoader,每个应用有自己的类加载器,优先加载自己的类,打破双亲委派。
- OSGi 等模块化框架。
破坏双亲委派通常是通过重写 loadClass 方法或使用线程上下文类加载器实现的。
面试加分点
- 能画出 JVM 内存结构图,并说明每部分作用。
- 清楚分代收集思想和常见收集器的适用场景(尤其是 G1)。
- 能讲清类加载过程和双亲委派的好处与破坏场景。
- 知道如何用 jmap、jstat、MAT 排查内存问题。
- 对锁升级、对象头有基本了解,能和 synchronized 关联起来。
- 实际项目中有过 JVM 参数调整或 OOM 排查经验,直接讲案例。
快速记忆框架:
JVM 基础面试核心:
内存结构 → 对象创建与内存分配 → GC 算法与收集器 → 类加载与双亲委派 → 引用类型 → OOM / 内存泄漏排查 → 常用调优参数
把内存结构和 GC 讲清楚,再能结合排查思路,基本就能覆盖大部分 JVM 基础面试。