← 返回面试专题导航

🔧 Java JVM 虚拟机面试题总览

掌握 JVM 内存模型、垃圾回收、类加载机制与调优

本页作为 JVM 专题的总览页,覆盖 14 道高频问题,并给出到各子专题页的跳转入口。

🎯 难度筛选

一、JVM 内存与错误

1. JVM 运行时数据区有哪些?各有什么作用? 中等

JVM 运行时数据区分为线程共享和线程私有两大类:

JVM 运行时数据区: ┌───────────────────────────────────┐ │ 线程共享区域 │ ├───────────────────────────────────┤ │ 堆(Heap) │ │ - 存放对象实例和数组 │ │ - GC 的主要工作区域 │ │ - 可通过 -Xms/-Xmx 调整大小 │ ├───────────────────────────────────┤ │ 方法区(Method Area/元空间) │ │ - 存放类元数据、常量、静态变量 │ │ - JDK8+ 用 Metaspace 替代永久代 │ │ - 可通过 -XX:MetaspaceSize 调整 │ └───────────────────────────────────┘ ┌───────────────────────────────────┐ │ 线程私有区域 │ ├───────────────────────────────────┤ │ 虚拟机栈(VM Stack) │ │ - 每个方法对应一个栈帧 │ │ - 存放局部变量表、操作数栈等 │ ├───────────────────────────────────┤ │ 本地方法栈(Native Method Stack) │ │ - 为 Native 方法服务 │ ├───────────────────────────────────┤ │ 程序计数器(PC Register) │ │ - 记录当前线程执行的字节码行号 │ │ - 唯一不会 OOM 的区域 │ └───────────────────────────────────┘

面试话术:"堆和方法区是所有线程共享的,主要参与 GC;栈、本地方法栈和 PC 是线程私有的,负责方法调用和执行流程控制。"

💡 记忆技巧:共享的是"数据"(对象、类信息),私有的是"执行现场"(栈帧、PC)。
2. 堆和栈的区别? 简单
┌──────────┬────────────────┬────────────────┐ │ 特性 │ 堆(Heap) │ 栈(Stack) │ ├──────────┼────────────────┼────────────────┤ │ 存储内容 │ 对象实例、数组 │ 局部变量、返回地址│ │ 生命周期 │ GC 自动回收 │ 方法结束自动释放│ │ 线程共享 │ 是(多线程共享)│ 否(线程独享) │ │ 大小 │ 较大且可调 │ 较小且固定 │ │ 异常 │ OutOfMemoryError│ StackOverflowError│ │ 分配效率 │ 较慢 │ 很快(指针碰撞)│ └──────────┴────────────────┴────────────────┘
public void foo() { int a = 10; // 栈:基本类型局部变量 String str = "hello"; // 栈:引用,常量池:"hello" Person p = new Person(); // 栈:引用 p,堆:Person 对象实例 // 方法结束后: // - 栈帧自动弹出,a/str/p 引用消失 // - 堆中的 Person 对象等待 GC 回收 }

易错点:"引用"本身在栈上,对象实例在堆上;基本类型的局部变量完全在栈上。

⚠️ 注意:成员变量(字段)跟随对象存储在堆上,不在栈上。
3. StackOverflowError 和 OutOfMemoryError 的区别? 中等

StackOverflowError(栈溢出):线程请求的栈深度超过虚拟机允许的最大深度。

常见原因:

  • 递归调用没有终止条件或递归层级过深
  • 方法调用链过长
// StackOverflowError 示例 public void recursion() { recursion(); // 无限递归,没有终止条件 } // 解决方案 public void recursion(int depth) { if (depth > 1000) return; // 添加终止条件 recursion(depth + 1); }

OutOfMemoryError(内存溢出):堆、元空间或其他内存区域无法分配足够的内存。

常见原因:

  • 堆内存不足:创建大量对象且无法被 GC 回收
  • 元空间不足:加载过多类
  • 直接内存不足:NIO 使用过多堆外内存
// OutOfMemoryError 示例 List<byte[]> list = new ArrayList<>(); while (true) { list.add(new byte[1024 * 1024]); // 不断往堆里塞对象 } // 解决方案 // 1. 增加堆内存:-Xmx2g // 2. 检查内存泄漏 // 3. 优化代码,及时释放资源
💡 排查参数:SOE 关注 -Xss(栈大小),OOM 关注 -Xmx(堆最大值)和 -XX:MaxMetaspaceSize(元空间)。

二、垃圾回收与收集器

4. JVM 常见的垃圾回收算法有哪些? 困难

垃圾回收的核心是"如何找到垃圾"和"如何回收垃圾",主要有以下四种算法:

1. 标记-清除(Mark-Sweep) 步骤:标记所有存活对象 → 清除未标记对象 优点:简单直接 缺点:产生内存碎片,影响大对象分配 2. 复制(Copying) 步骤:将内存分为两块,每次只用一块 存活对象复制到另一块 → 清空当前块 优点:无碎片,分配快 缺点:浪费一半内存,存活对象多时效率低 3. 标记-整理(Mark-Compact) 步骤:标记存活对象 → 将存活对象向一端移动 → 清理边界外内存 优点:无碎片,不浪费空间 缺点:移动对象成本高,需要更新引用 4. 分代收集(Generational) 核心思想:根据"大部分对象朝生夕死"的特性,将堆分代管理 新生代(Young Gen):使用复制算法,回收频繁但速度快 老年代(Old Gen):使用标记-清除或标记-整理,回收频率低

现代 JVM 基本都采用分代收集,根据对象存活时间选择不同算法,兼顾效率和空间利用率。

💡 记忆口诀:标清有碎片,复制浪费空间,标整成本高,分代最实用。
5. 常见的垃圾收集器有哪些?如何选择? 困难

垃圾收集器是 GC 算法的具体实现,不同收集器适用于不同场景:

新生代收集器: - Serial:单线程,STW,适合客户端小堆 - ParNew:Serial 的多线程版本,配合 CMS 使用 - Parallel Scavenge:吞吐量优先,适合后台批处理 老年代收集器: - Serial Old:单线程,标记-整理 - Parallel Old:Parallel Scavenge 的老年代版本 - CMS(Concurrent Mark Sweep):并发标记清除,低延迟,但有碎片 全堆收集器: - G1(Garbage First):分 Region 管理,可预测停顿,JDK9+ 默认 - ZGC:超低延迟(< 10ms),支持 TB 级堆 - Shenandoah:类似 ZGC,低延迟

选择建议:

  • 小堆(< 4G):Parallel Scavenge + Parallel Old(吞吐量优先)
  • 中大堆(4G-32G):G1(平衡吞吐量和延迟)
  • 超大堆或极致低延迟:ZGC / Shenandoah
# G1 配置示例(推荐) -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m # ZGC 配置示例(JDK 11+) -XX:+UseZGC -XX:ZCollectionInterval=120

面试话术:"JDK 9 之后默认用 G1,它通过分 Region 管理堆,可以设置最大停顿时间目标,适合大多数服务端应用。"

6. Minor GC / Major GC / Full GC 区别? 中等
┌──────────┬──────────────┬──────────────────┐ │ GC 类型 │ 回收区域 │ 触发条件 │ ├──────────┼──────────────┼──────────────────┤ │ Minor GC │ 新生代(Eden) │ Eden 区满 │ │ Major GC │ 老年代 │ 老年代空间不足 │ │ Full GC │ 整个堆+元空间 │ 多种情况(见下) │ └──────────┴──────────────┴──────────────────┘

Minor GC(Young GC):

  • 只回收新生代,频率高但速度快(毫秒级)
  • 触发时机:Eden 区满时
  • 存活对象会晋升到 Survivor 或老年代

Major GC:

  • 只回收老年代,有些收集器(如 CMS)会单独触发
  • 很多情况下 Major GC 和 Full GC 混用

Full GC:

  • 回收整个堆(新生代+老年代)+ 元空间
  • 停顿时间长,对性能影响大
  • 触发条件:
    • 老年代空间不足
    • 元空间不足
    • System.gc() 显式调用(不推荐)
    • CMS GC 失败(Concurrent Mode Failure)
⚠️ 性能警告:频繁 Full GC 是严重的性能问题,需要重点排查(内存泄漏、堆太小、对象晋升过快等)。

观察 GC 日志时:Minor GC 频繁是正常的,但如果 Full GC 频率高(如每分钟多次)或单次停顿超过 1 秒,就需要调优了。

三、类加载与双亲委派

7. 类加载的完整过程是什么? 困难

类加载分为加载、连接、初始化三大阶段,连接又细分为验证、准备、解析:

类加载的完整过程: 1. 加载(Loading) - 通过类的全限定名获取二进制字节流 - 将字节流转化为方法区的运行时数据结构 - 在堆中生成 Class 对象作为访问入口 2. 验证(Verification) - 文件格式验证(魔数、版本号等) - 元数据验证(语义检查) - 字节码验证(数据流和控制流分析) - 符号引用验证 3. 准备(Preparation) - 为类变量(static)分配内存并设置默认初始值 - 注意:这里是默认值(如 int 为 0),不是显式赋的值 4. 解析(Resolution) - 将常量池中的符号引用替换为直接引用 5. 初始化(Initialization) - 执行类构造器 <clinit>() 方法 - 执行静态代码块和静态变量赋值 - 父类先于子类初始化
public class Test { // 准备阶段:value = 0(默认值) // 初始化阶段:value = 123(显式赋值) public static int value = 123; static { // 初始化阶段执行 System.out.println("Static block"); } }
💡 记忆口诀:加(载)验准解初,准备给默认值,初始化才赋真值。
8. 双亲委派模型的设计目的?什么时候需要打破? 困难

双亲委派模型:类加载器收到加载请求时,先委派给父加载器,父加载器无法加载时才自己加载。

类加载器层次结构: Bootstrap ClassLoader(启动类加载器) ↑ 委派 Extension ClassLoader(扩展类加载器) ↑ 委派 Application ClassLoader(应用类加载器) ↑ 委派 Custom ClassLoader(自定义类加载器)

设计目的:

  • 避免重复加载:父加载器已加载的类,子加载器不会再加载
  • 保护核心类库:防止核心类被篡改(如自己写个 java.lang.String 也无法替换 JDK 的)

什么时候需要打破双亲委派?

  • JDBC:DriverManager 在启动类加载器中,但驱动实现在应用类路径,需要通过线程上下文类加载器加载
  • Tomcat:每个 Web 应用需要隔离,同一个类可以有多个版本
  • OSGi / 模块化:需要更灵活的类加载机制
⚠️ 注意:打破双亲委派需要重写 loadClass 方法,而不是 findClass。

四、JVM 参数、调优与引用类型

9. 常用的 JVM 参数有哪些? 中等

JVM 参数分为三类:标准参数(-)、非标准参数(-X)、不稳定参数(-XX)。

# 堆内存设置 -Xms2g # 初始堆大小 -Xmx4g # 最大堆大小 -Xmn1g # 新生代大小 -XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1 # 栈内存设置 -Xss256k # 每个线程栈大小 # 元空间设置(JDK 8+) -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m # GC 相关 -XX:+UseG1GC # 使用 G1 收集器 -XX:MaxGCPauseMillis=200 # 最大 GC 停顿时间目标 -XX:+PrintGCDetails # 打印 GC 详情(JDK 8) -Xlog:gc*:file=gc.log # GC 日志(JDK 9+) -XX:+HeapDumpOnOutOfMemoryError # OOM 时生成堆转储 -XX:HeapDumpPath=/tmp/dump.hprof # 性能调优 -XX:+DisableExplicitGC # 禁用 System.gc() -XX:+UseStringDeduplication # 字符串去重(G1)
💡 生产推荐:-Xms 和 -Xmx 设置相同,避免堆动态扩容带来的性能抖动。
10. OOM 通常怎么排查? 中等

OOM 排查的标准流程:

  1. 生成堆转储(Heap Dump):
    # 启动时配置自动生成 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof # 手动生成 jmap -dump:format=b,file=heapdump.hprof <pid>
  2. 分析堆转储文件:
    • 使用 MAT(Memory Analyzer Tool)或 VisualVM 打开 dump 文件
    • 查看"Leak Suspects"报告,找出占用内存最多的对象
    • 分析对象的引用链,找出为什么无法被 GC 回收
  3. 查看 GC 日志:
    # 实时监控 GC jstat -gcutil <pid> 1000 # 每秒打印一次 # 分析 GC 日志 # 关注 Full GC 频率、老年代使用率趋势
  4. 定位问题并修复:
    • 内存泄漏:修复代码,及时释放资源
    • 堆太小:增加 -Xmx
    • 对象创建过多:优化代码,减少临时对象
💡 常用工具:jmap / jstat / MAT / VisualVM / Arthas
11. 什么是内存泄漏?在 JVM 中有哪些常见场景? 简单

内存泄漏(Memory Leak):程序已经不再使用某些对象,但这些对象依然有引用链可达,JVM 无法回收它们,导致堆占用越来越高,最终可能抛出 OutOfMemoryError。

在 JVM 中常见的几种场景:

  • 静态集合:例如 static List/Map 持有大量对象,但从不清空,随着请求增多集合只增不减。
  • 缓存实现不当:自己用 HashMap 做缓存,却没有淘汰策略;推荐使用 Caffeine、Guava Cache 或 WeakHashMap 等。
  • 监听器/回调未移除:对象注册到全局事件总线、Observer、Listener 列表中,生命周期结束后忘记取消注册。
  • ThreadLocal 未清理:在线程池中使用 ThreadLocal,只 setremove,线程长期存在时其 ThreadLocalMap 条目也一直存活。
  • 资源未关闭:JDBC 连接、ResultSet、IO 流没有在 finally/try-with-resources 中关闭,背后一整条对象链都无法被回收。

避免策略:控制集合大小并显式清理;合理使用弱引用/软引用做缓存;为监听器提供明确的注销逻辑;在线程池场景中使用完 ThreadLocal 后立刻 remove();对所有外部资源使用 try-with-resources 自动关闭。

12. 强引用、软引用、弱引用、虚引用的区别? 中等

从 GC 回收的“优先级”来看:强引用 > 软引用 > 弱引用 > 虚引用。

┌──────────┬──────────────┬────────────────┐ │ 类型 │ 回收时机 │ 典型使用场景 │ ├──────────┼──────────────┼────────────────┤ │ 强引用 │ 只要有引用就不回收 │ 普通业务对象 │ │ 软引用 │ 内存吃紧时才回收 │ 本地缓存(图片等)│ │ 弱引用 │ 下次 GC 必回收 │ WeakHashMap 键 │ │ 虚引用 │ 随时可回收 │ 监控回收、堆外内存│ └──────────┴──────────────┴────────────────┘
// 强引用(默认) Object strong = new Object(); // 软引用:内存吃紧时 GC SoftReference<byte[]> soft = new SoftReference<>(new byte[1024]); // 弱引用:下一次 GC 一定会回收 WeakReference<Object> weak = new WeakReference<>(new Object()); // 虚引用:无法通过 get() 取得对象,只能配合 ReferenceQueue 监控回收 ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);

面试话术:先给出一个表格,再补一句“软引用常用于内存敏感缓存、弱引用常用于 Map 键自动过期、虚引用主要做资源清理和监控”。

13. JVM 调优的一般思路? 困难

一套通用的 JVM 调优 checklist:

  1. 先定目标:是降低 P99 延迟、提升 QPS,还是减少 Full GC?没有量化目标的调优都是“拍脑袋”。
  2. 采集数据:打开 GC 日志(-Xlog:gc*-XX:+PrintGCDetails),同时监控 CPU、内存、线程数和业务 RT/QPS。
  3. 分析瓶颈:
    • 是 Minor GC 过于频繁,还是单次 Full GC 停顿过长?
    • 老年代是否持续上涨(可能有内存泄漏)?
    • 对象创建是否过多(逃逸分析无效、大量装箱等)?
  4. 制定方案:代码参数两个层面考虑:
    • 减少临时对象、复用缓冲区、合理使用对象池。
    • 调整堆/新生代大小、选择合适 GC(Parallel/G1/ZGC)。
  5. 小步迭代 + 压测验证:每次只改少量参数或一处热点代码,配合压测对比调优前后的 GC 次数、停顿时间和业务指标。

经验总结:优先优化代码,其次调 JVM 参数,最后再考虑加机器。不要一上来就堆硬件。

14. 排查 JVM 问题时常用哪些工具? 中等

命令行工具:

  • jps:列出当前 Java 进程,排查 PID。
  • jstat:查看 GC/内存使用(jstat -gcutil pid 1000)。
  • jmap:导出 heap dump、查看堆结构。
  • jstack:打印线程栈,定位死锁和 CPU 飙高线程。
  • jinfo:查看/修改 JVM 参数。
  • jcmd:综合诊断工具,新版很多功能只在它里面提供。

图形/在线工具:

  • VisualVM / JMC:JDK 自带或官方提供的图形化分析工具。
  • Arthas:阿里开源的线上诊断神器,支持实时查看线程、内存、调用链。

面试时可以拿一个真实排查案例(例如“线程暴涨 + Full GC 频繁”)简单讲一下你是如何配合这些工具一步步定位问题的,会非常加分。