Administrator 9 阅读 面试

JVM 基础高频面试题(含口述版回答)


1. 讲讲 JVM 的内存结构(运行时数据区)

口述版:

JVM 运行时数据区主要分为以下几块:

线程私有:

  • 程序计数器:记录当前线程执行到的字节码行号,线程切换后能恢复。唯一不会 OOM 的区域。
  • 虚拟机栈:每个方法执行时创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等。栈溢出抛 StackOverflowError。
  • 本地方法栈:为 Native 方法服务,和虚拟机栈类似。

线程共享:

  • 堆(Heap):存放对象实例和数组,是 GC 的主要区域。分为新生代和老年代。
  • 方法区(Metaspace):存储类信息、常量、静态变量、JIT 编译后的代码等。JDK 8 后用元空间(本地内存)替代永久代。

另外还有直接内存(堆外内存),NIO 会用到,不在运行时数据区里,但也会导致 OOM。


2. 堆和栈的区别?

口述版:

对比点 堆(Heap) 栈(Stack)
存放内容 对象实例、数组 局部变量、方法调用信息
共享性 线程共享 线程私有
大小 较大,可动态调整 较小,固定或有限增长
异常 OutOfMemoryError StackOverflowError
管理方式 GC 自动回收 方法结束自动弹出栈帧
速度 相对慢 很快

简单记:对象在堆,方法调用在栈。栈的生命周期随方法,堆的生命周期由 GC 决定。


3. 对象的创建过程是怎样的?

口述版:

以 new 一个对象为例,大致流程:

  1. 类加载检查:检查类是否已加载,没有则先加载。
  2. 分配内存:在堆中划分一块空间(指针碰撞或空闲列表)。
  3. 初始化零值:把对象的实例变量设为默认值(0、null 等)。
  4. 设置对象头:写入 Mark Word、类型指针、数组长度等信息。
  5. 执行 <init> 方法:调用构造方法,进行真正的初始化。

并发分配内存时,用 CAS + 失败重试,或者用 TLAB(线程本地分配缓冲)减少竞争。


4. 什么是 GC Roots?哪些对象可以作为 GC Roots?

口述版:

GC Roots 是垃圾回收时的起始点,通过可达性分析判断对象是否存活。从 GC Roots 出发,无法到达的对象就可以被回收。

常见的 GC Roots 包括:

  1. 虚拟机栈中引用的对象(局部变量)
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中 JNI 引用的对象
  5. 被同步锁持有的对象
  6. JVM 内部的一些特殊对象(如系统类加载器)

5. 常见的垃圾回收算法有哪些?

口述版:

  1. 标记-清除(Mark-Sweep):标记存活对象,再清除未标记的。缺点是产生内存碎片。
  2. 标记-复制(Mark-Copy):把存活对象复制到另一块空间,然后清空原空间。适合新生代(存活对象少),缺点是浪费一半空间。
  3. 标记-整理(Mark-Compact):标记后把存活对象向一端移动,清理边界外的内存。适合老年代,无碎片但移动成本高。
  4. 分代收集:根据对象生命周期把堆分成新生代和老年代,采用不同算法。这是现代 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 的常见原因:

  1. 老年代空间不足
  2. 方法区(Metaspace)空间不足
  3. 调用 System.gc()(建议别主动调)
  4. 晋升失败、并发模式失败(CMS)等

8. 类加载过程是怎样的?什么是双亲委派模型?

口述版:

类加载过程分三个大步骤:

  1. 加载(Loading):通过类名获取字节流,转换成方法区的运行时结构,在堆中生成 Class 对象。
  2. 链接(Linking):
    • 验证:确保字节码安全
    • 准备:为静态变量分配内存并设零值
    • 解析:把符号引用转为直接引用
  3. 初始化(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 无法回收,导致可用内存逐渐减少。

泄漏最终往往会导致溢出。

常见泄漏场景:

  1. 静态集合一直持有对象引用
  2. 监听器、回调没被移除
  3. ThreadLocal 使用完没 remove
  4. 连接、流等资源没关闭
  5. 内部类持有外部类引用

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 和内存问题?

口述版:

一般步骤:

  1. 加上 -XX:+HeapDumpOnOutOfMemoryError,OOM 时自动 dump。
  2. 用 jmap 或 jcmd 手动生成堆转储。
  3. 用 MAT(Memory Analyzer) 或 VisualVM 分析 dump 文件,找占用内存最大的对象和引用链。
  4. 结合 jstat 看 GC 情况,jstack 看线程状态。
  5. 如果是堆外内存问题,可能要看 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 是干什么的?

口述版:

对象在内存中分为三部分:

  1. 对象头
  2. 实例数据
  3. 对齐填充

对象头又包括:

  • Mark Word:存储哈希码、GC 分代年龄、锁状态、偏向线程 ID 等信息。锁升级(偏向锁 → 轻量级锁 → 重量级锁)主要改的就是这里的状态位。
  • 类型指针(Klass Pointer):指向类的元数据。
  • 如果是数组,还有数组长度。

了解 Mark Word 有助于理解 synchronized 的锁升级过程。


15. 双亲委派模型会被破坏吗?举例说明

口述版:

会被破坏,常见场景:

  1. SPI 机制(如 JDBC):核心类(DriverManager)需要加载厂商实现的驱动类,由启动类加载器委托上下文类加载器去加载。
  2. 热部署、热加载:像 Tomcat 的 WebAppClassLoader,每个应用有自己的类加载器,优先加载自己的类,打破双亲委派。
  3. OSGi 等模块化框架。

破坏双亲委派通常是通过重写 loadClass 方法或使用线程上下文类加载器实现的。


面试加分点

  1. 能画出 JVM 内存结构图,并说明每部分作用。
  2. 清楚分代收集思想和常见收集器的适用场景(尤其是 G1)。
  3. 能讲清类加载过程和双亲委派的好处与破坏场景。
  4. 知道如何用 jmap、jstat、MAT 排查内存问题。
  5. 对锁升级、对象头有基本了解,能和 synchronized 关联起来。
  6. 实际项目中有过 JVM 参数调整或 OOM 排查经验,直接讲案例。

快速记忆框架:

JVM 基础面试核心:

内存结构 → 对象创建与内存分配 → GC 算法与收集器 → 类加载与双亲委派 → 引用类型 → OOM / 内存泄漏排查 → 常用调优参数

把内存结构和 GC 讲清楚,再能结合排查思路,基本就能覆盖大部分 JVM 基础面试。