Skip to content

JVM 面试题

JVM 基础

Q1: JVM 内存结构分为哪些区域?

┌─────────────────────────────────────────────────────────────────┐
│                    JVM 运行时数据区                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   线程共享                                                      │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │                    Heap(堆)                             │  │
│   │   • 对象实例 • 数组                                      │  │
│   └─────────────────────────────────────────────────────────┘  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │                  Method Area(方法区)                    │  │
│   │   • 类信息 • 常量 • 静态变量 • JIT 编译后代码            │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                  │
│   线程私有                                                      │
│   ┌───────────────────┐    ┌───────────────────────────────┐  │
│   │   JVM Stack       │    │    Native Method Stack        │  │
│   │   (虚拟机栈)       │    │    (本地方法栈)                │  │
│   │   • 方法调用       │    │    • native 方法调用           │  │
│   │   • 局部变量       │    │                              │  │
│   └───────────────────┘    └───────────────────────────────┘  │
│                                                                  │
│   ┌───────────────────┐    ┌───────────────────────────────┐  │
│   │   Program Counter │    │    Native Method Interface    │  │
│   │   Register (PC)   │    │    (本地方法接口)              │  │
│   └───────────────────┘    └───────────────────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
区域线程共享存储内容异常
Heap对象实例、数组OOM
Method Area类信息、常量、静态变量OOM
JVM Stack局部变量、方法参数SOF、OOM
PC Register当前字节码行号-
Native Stacknative 方法参数SOF、OOM

Q2: 堆内存分为哪些区域?

┌─────────────────────────────────────────────────────────────────┐
│                         堆内存结构                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   JDK 7 及之前:                                                  │
│   ┌───────────┬───────────┬───────────────┐                     │
│   │   Eden    │  Survivor │    Old Gen   │                     │
│   │   8/10    │  1/10     │    9/10      │                     │
│   │   (伊甸)  │  (幸存区)  │    (老年代)   │                     │
│   └───────────┴───────────┴───────────────┘                     │
│                                                                  │
│   JDK 8+:                                                        │
│   ┌───────────┬───────────┬───────────────┐                     │
│   │   Eden    │  Survivor │    Old Gen   │                     │
│   │   8/10    │  1/10     │    9/10      │                     │
│   └───────────┴───────────┴───────────────┘                     │
│                        ↓                                        │
│                   Metaspace(元空间)                             │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

JDK 7 vs JDK 8 区别

  • JDK 7:方法区在 PermGen(永久代),大小固定
  • JDK 8:方法区在 Metaspace(元空间),使用本地内存,可动态调整

Q3: 什么是 OOM?如何排查?

OOM 类型

OOM 类型原因解决方案
Java heap space对象太多增大堆,检查内存泄漏
GC overhead limit exceededGC 太频繁减少对象创建
Metaspace类太多/CGLib增大元空间
Unable to create native thread线程太多减少线程数
Direct buffer memoryNIO 直接内存限制大小

排查步骤

┌─────────────────────────────────────────────────────────────────┐
│                      OOM 排查流程                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 添加 JVM 参数保留堆 dump                                    │
│     -XX:+HeapDumpOnOutOfMemoryError                            │
│     -XX:HeapDumpPath=/path/to/dump.hprof                       │
│                                                                  │
│  2. 重现问题,生成 dump 文件                                    │
│                                                                  │
│  3. 使用 MAT 分析 dump                                          │
│     - Histogram: 按对象数量/大小排序                            │
│     - Dominator Tree: 对象引用关系                              │
│                                                                  │
│  4. 定位问题代码                                                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

垃圾回收

Q4: 垃圾回收的常见算法?

1. 引用计数法

java
// 每个对象有个引用计数器
// 引用 +1,失效 -1
// 计数器为0则回收
// ❌ 无法处理循环引用

2. 可达性分析法(Java 使用)

java
// 从 GC Root 开始,向下搜索
// GC Root 包括:
// - 虚拟机栈中引用的对象
// - 方法区中类静态属性引用的对象
// - 方法区中常量引用的对象
// - 本地方法栈中 JNI 引用的对象

3. 标记-清除算法

标记存活对象 → 清除未标记对象
缺点:产生内存碎片

4. 复制算法

内存分两块 From/To
存活对象复制到 To Space
清空 From Space
优点:无碎片,简单高效
缺点:浪费一半空间

5. 标记-整理算法

标记存活对象 → 整理到一端 → 清除边界外对象
优点:无碎片
缺点:移动对象,开销较大

Q5: 分代收集算法?

┌─────────────────────────────────────────────────────────────────┐
│                      分代收集策略                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   新生代 (Young Gen)                                             │
│   ├── Eden Space (8/10)                                         │
│   └── Survivor Space (1/10 × 2)                                 │
│         ↑                                                           │
│         │  Minor GC(频繁,停顿短)                               │
│         │                                                           │
│         ▼                                                           │
│   老年代 (Old Gen)                                               │
│         │                                                           │
│         │  Major GC / Full GC(频率低,停顿长)                   │
│         │                                                           │
│         ▼                                                           │
│   永久代/元空间 (方法区)                                          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

分代假说

  1. 弱分代假说:大多数对象朝生夕死
  2. 强分代假说:熬过多次 GC 的对象越难死亡
  3. 跨代引用假说:新生代对象可能被老年代引用

Q6: 常见的垃圾收集器?

┌─────────────────────────────────────────────────────────────────┐
│                      垃圾收集器对比                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   新生代收集器:                                                  │
│   ├── Serial - 单线程,Client 模式                               │
│   ├── ParNew - 多线程,Server 模式(CMS 搭档)                   │
│   └── Parallel Scavenge - 吞吐量优先                             │
│                                                                  │
│   老年代收集器:                                                  │
│   ├── Serial Old - 单线程                                       │
│   ├── Parallel Old - 多线程                                     │
│   └── CMS - 并发标记清除,追求低停顿                             │
│                                                                  │
│   不分代收集器:                                                  │
│   ├── G1 - JDK 9+ 默认                                         │
│   └── ZGC - 大内存,低停顿(毫秒级)                             │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

常用组合

组合新生代老年代说明
Serial + Serial OldSerialSerial OldClient 模式
ParNew + CMSParNewCMS追求低停顿
PS + POParallel ScavengeParallel Old追求高吞吐
G1G1G1JDK 9+ 默认

Q7: CMS 收集器的工作流程?

┌─────────────────────────────────────────────────────────────────┐
│                    CMS 收集器工作流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   1. 初始标记 (STW)      - 标记 GC Root 直接引用的对象           │
│            ↓                                                     │
│   2. 并发标记           - 遍历对象图(用户线程一起跑)           │
│            ↓                                                     │
│   3. 重新标记 (STW)      - 修正并发标记期间变动                 │
│            ↓                                                     │
│   4. 并发清除           - 清除死亡对象(用户线程一起跑)         │
│                                                                  │
│   优点:并发收集,低停顿                                          │
│   缺点:产生内存碎片,CPU敏感                                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Q8: G1 收集器原理?

┌─────────────────────────────────────────────────────────────────┐
│                    G1 收集器原理                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   G1 把堆分成多个大小相等的 Region:                               │
│                                                                  │
│   ┌──────┬──────┬──────┬──────┐                                 │
│   │ Eden │ Eden │  S   │ Eden │                                 │
│   ├──────┼──────┴──────┼──────┤                                 │
│   │  S   │    Old      │  S   │   S = Survivor                  │
│   ├──────┼─────────────┼──────┤                                 │
│   │  H   │    Old      │  H   │   H = Humongous (大对象)         │
│   └──────┴─────────────┴──────┘                                 │
│                                                                  │
│   特点:                                                          │
│   - 并行与并发                                                    │
│   - 分代收集                                                      │
│   - 空间整合(无碎片)                                            │
│   - 可预测停顿(-XX:MaxGCPauseMillis)                          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Q9: 什么情况下会触发 Full GC?

触发条件

  1. 老年代空间不足
  2. System.gc()(不确定,由 JVM 决定)
  3. Metaspace 空间不足(JDK 8+)
  4. Minor GC 时 Survivor 区放不下对象
  5. CMS GC 时浮动垃圾导致 concurrent mode failure

优化建议

bash
# 合理设置堆大小
-Xms4g -Xmx4g

# 合理设置新生代
-Xmn2g

# 设置 Eden 和 Survivor 比例
-XX:SurvivorRatio=8

Q10: Minor GC 和 Full GC 的区别?

对比Minor GCFull GC
触发区域新生代老年代 + 方法区
触发条件Eden 满多种条件
停顿时间
频率
是否StopTheWorld是(但很短)是(较长)

类加载

Q11: 类加载的过程?

┌─────────────────────────────────────────────────────────────────┐
│                       类加载流程                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   加载 ──▶ 验证 ──▶ 准备 ──▶ 解析 ──▶ 初始化 ──▶ 使用 ──▶ 卸载 │
│                                                                  │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                     验证阶段                            │   │
│   │  文件格式验证 → 元数据验证 → 字节码验证 → 符号引用验证   │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

加载阶段

  1. 通过类全限定名获取二进制字节流
  2. 将字节流转化为方法区的运行时数据结构
  3. 在堆中生成 Class 对象

初始化时机

java
// 1. new 实例
new MyClass();

// 2. 访问静态成员
System.out.println(MyClass.count);

// 3. 反射
Class.forName("com.example.MyClass");

// 4. 初始化子类会触发父类
class Parent { static { System.out.println("Parent init"); } }
class Child extends Parent { }
new Child();  // 先输出 Parent init

Q12: 什么是双亲委派模型?

┌─────────────────────────────────────────────────────────────────┐
│                    双亲委派模型                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│                        Bootstrap ClassLoader                      │
│                         (C++ 实现,无法访问)                      │
│                              ▲                                   │
│                              │                                   │
│                        Extension ClassLoader                      │
│                        (加载 JDK 扩展包)                         │
│                              ▲                                   │
│                              │                                   │
│                        Application ClassLoader                    │
│                        (加载 classpath 中的类)                    │
│                              ▲                                   │
│                              │                                   │
│                      自定义 ClassLoader                          │
│                                                                  │
│   加载请求向上传递,父类无法加载才由子类加载                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

好处

  1. 安全性:核心类(如 java.lang.String)由 Bootstrap 加载,不会被替换
  2. 避免重复加载:父类已加载的类不会被子类重复加载

Q13: 如何打破双亲委派?

热部署场景(Tomcat):

java
public class TomcatClassLoader extends ClassLoader {

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {

        // 判断是否由自己加载
        if (name.startsWith("com.myapp.")) {
            return findClass(name);  // 直接自己加载,不委派
        }

        // 其他类仍走双亲委派
        return super.loadClass(name, resolve);
    }
}

SPI 机制(JDBC 驱动加载):

java
// Thread.currentThread().getContextClassLoader() 可以获取当前线程的 ClassLoader
// 用于打破双亲委派,让子类加载器能加载父类加载器的类

Q14: ClassLoader 的源码?

java
public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {

        // 1. 先检查是否已加载
        Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
            return clazz;
        }

        // 2. 委派给父类加载
        try {
            ClassLoader parent = getParent();
            if (parent != null) {
                clazz = parent.loadClass(name, false);
            } else {
                clazz = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) { }

        // 3. 父类无法加载,自己加载
        if (clazz == null) {
            clazz = findClass(name);
        }

        return clazz;
    }

    @Override
    protected Class<?> findClass(String name) {
        // 自定义类加载逻辑
        byte[] bytes = loadClassData(name);
        return defineClass(name, bytes, 0, bytes.length);
    }
}

JVM 调优

Q15: 常用 JVM 参数?

堆内存参数

bash
-Xms256m          # 初始堆大小
-Xmx512m          # 最大堆大小
-Xmn128m          # 新生代大小
-Xss1m            # 线程栈大小

元空间参数(JDK 8+):

bash
-XX:MetaspaceSize=256m    # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大

GC 参数

bash
-XX:+UseG1GC             # 使用 G1 收集器
-XX:MaxGCPauseMillis=200 # 最大 GC 停顿时间
-XX:+PrintGCDetails      # 打印 GC 详情
-Xloggc:/path/to/gc.log  # GC 日志文件

OOM 参数

bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof

Q16: 如何查看 JVM 参数?

bash
# JPS - 查看 Java 进程
jps -l
jps -v | grep heap

# JINFO - 查看/修改配置
jinfo -flags <pid>            # 查看所有参数
jinfo -flag MaxHeapSize <pid> # 查看某个参数

# JSTAT - 监控 JVM 统计信息
jstat -gcutil <pid> 1000 10   # 每秒一次,共10次

# JMAP - 导出堆转储
jmap -dump:format=b,file=heap.hprof <pid>
jmap -heap <pid>               # 堆配置和使用

Q17: GC 日志分析?

GC 日志参数

bash
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log

GC 日志解读

2024-01-15T10:30:45.123+0800: [GC (Allocation Failure)
  [PSYoungGen: 1024K->128K(1536K)] 2048K->512K(4096K)
  0.015ms]
字段说明
Allocation Failure触发原因:对象分配失败
PSYoungGen新生代垃圾收集器(Parallel Scavenge)
1024K->128K收集前->收集后
(1536K)总容量
2048K->512K堆内存使用 2M->512K

Q18: 常见的 OOM 场景和解决方案?

1. Java heap space

原因:对象太多,堆内存不足
解决:
  -Xmx 增大堆
  - 检查内存泄漏(MAT 分析)
  - 优化代码,减少对象创建

2. Metaspace

原因:类太多(JDK 8+)
解决:
  -XX:MaxMetaspaceSize 增大
  - 检查 CGLib 动态生成类
  - 检查是否频繁加载类

3. Unable to create native thread

原因:线程太多
解决:
  - 减少线程数
  - 减小堆内存(线程占用栈空间)
  - 减小 -Xss

Q19: Arthas 常用命令?

bash
# 启动 Arthas
java -jar arthas-boot.jar

# 监控方法调用
watch com.example.UserService getUser "{params, returnObj}"

# 查看方法耗时
trace com.example.UserService getUser

# 查看线程状态
thread

# 查看加载的类
sc com.example.*

# 反编译类
jad com.example.UserService

# 动态修改日志级别
logger -c <class>

Q20: CPU 100% 如何排查?

bash
# 1. 找到 CPU 高的进程
jps -l

# 2. 查看线程
top -Hp <pid>

# 3. 导出线程堆栈
jstack <pid> > thread.log

# 4. 找到高 CPU 线程
# 将线程 ID 转为 16 进制

# 5. 在 thread.log 中搜索该 16 进制 ID

实战问题

Q21: 如何定位 Full GC 频繁?

症状

  • Full GC 次数多
  • FGCT 累积增长快

排查步骤

bash
# 1. 查看 GC 日志
jstat -gcutil <pid> 1000

# 2. 分析 GC 日志
# - Minor GC 频率
# - Full GC 频率
# - 老年代内存使用率

# 3. 如果是老年代满导致
# - 增大老年代 -Xmn
# - 检查是否有内存泄漏

Q22: 如何优化 GC?

原则

  1. 先分析,再调优
  2. 避免过早优化
  3. 监控是优化的前提

常用优化方向

场景优化方案
Minor GC 频繁增大 Eden 区
Full GC 频繁检查内存泄漏 / 增大堆
GC 停顿长使用 G1 / ZGC
吞吐量低使用 Parallel GC

Q23: 生产环境 JVM 参数设置?

bash
# 通用服务器配置
java -server \
  -Xms4g -Xmx4g \                 # 堆大小
  -Xmn2g \                        # 新生代
  -XX:+UseG1GC \                  # G1 收集器
  -XX:MaxGCPauseMillis=200 \      # 最大停顿时间
  -XX:+HeapDumpOnOutOfMemoryError \  # OOM 时 dump
  -XX:HeapDumpPath=/var/log/heap.hprof \
  -Xloggc:/var/log/gc.log \       # GC 日志
  -XX:+PrintGCDateStamps \
  -XX:+PrintGCDetails \
  -jar app.jar

总结

JVM 高频面试知识点

知识点面试频率
JVM 内存结构⭐⭐⭐⭐⭐
垃圾回收算法⭐⭐⭐⭐⭐
分代收集原理⭐⭐⭐⭐⭐
常见垃圾收集器⭐⭐⭐⭐
类加载机制⭐⭐⭐⭐⭐
双亲委派模型⭐⭐⭐⭐⭐
OOM 排查⭐⭐⭐⭐
JVM 参数调优⭐⭐⭐⭐