Appearance
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 Stack | 否 | native 方法参数 | 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 exceeded | GC 太频繁 | 减少对象创建 |
| Metaspace | 类太多/CGLib | 增大元空间 |
| Unable to create native thread | 线程太多 | 减少线程数 |
| Direct buffer memory | NIO 直接内存 | 限制大小 |
排查步骤:
┌─────────────────────────────────────────────────────────────────┐
│ 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(频率低,停顿长) │
│ │ │
│ ▼ │
│ 永久代/元空间 (方法区) │
│ │
└─────────────────────────────────────────────────────────────────┘分代假说:
- 弱分代假说:大多数对象朝生夕死
- 强分代假说:熬过多次 GC 的对象越难死亡
- 跨代引用假说:新生代对象可能被老年代引用
Q6: 常见的垃圾收集器?
┌─────────────────────────────────────────────────────────────────┐
│ 垃圾收集器对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 新生代收集器: │
│ ├── Serial - 单线程,Client 模式 │
│ ├── ParNew - 多线程,Server 模式(CMS 搭档) │
│ └── Parallel Scavenge - 吞吐量优先 │
│ │
│ 老年代收集器: │
│ ├── Serial Old - 单线程 │
│ ├── Parallel Old - 多线程 │
│ └── CMS - 并发标记清除,追求低停顿 │
│ │
│ 不分代收集器: │
│ ├── G1 - JDK 9+ 默认 │
│ └── ZGC - 大内存,低停顿(毫秒级) │
│ │
└─────────────────────────────────────────────────────────────────┘常用组合:
| 组合 | 新生代 | 老年代 | 说明 |
|---|---|---|---|
| Serial + Serial Old | Serial | Serial Old | Client 模式 |
| ParNew + CMS | ParNew | CMS | 追求低停顿 |
| PS + PO | Parallel Scavenge | Parallel Old | 追求高吞吐 |
| G1 | G1 | G1 | JDK 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?
触发条件:
- 老年代空间不足
- System.gc()(不确定,由 JVM 决定)
- Metaspace 空间不足(JDK 8+)
- Minor GC 时 Survivor 区放不下对象
- CMS GC 时浮动垃圾导致 concurrent mode failure
优化建议:
bash
# 合理设置堆大小
-Xms4g -Xmx4g
# 合理设置新生代
-Xmn2g
# 设置 Eden 和 Survivor 比例
-XX:SurvivorRatio=8Q10: Minor GC 和 Full GC 的区别?
| 对比 | Minor GC | Full GC |
|---|---|---|
| 触发区域 | 新生代 | 老年代 + 方法区 |
| 触发条件 | Eden 满 | 多种条件 |
| 停顿时间 | 短 | 长 |
| 频率 | 高 | 低 |
| 是否StopTheWorld | 是(但很短) | 是(较长) |
类加载
Q11: 类加载的过程?
┌─────────────────────────────────────────────────────────────────┐
│ 类加载流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 加载 ──▶ 验证 ──▶ 准备 ──▶ 解析 ──▶ 初始化 ──▶ 使用 ──▶ 卸载 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 验证阶段 │ │
│ │ 文件格式验证 → 元数据验证 → 字节码验证 → 符号引用验证 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘加载阶段:
- 通过类全限定名获取二进制字节流
- 将字节流转化为方法区的运行时数据结构
- 在堆中生成 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 initQ12: 什么是双亲委派模型?
┌─────────────────────────────────────────────────────────────────┐
│ 双亲委派模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Bootstrap ClassLoader │
│ (C++ 实现,无法访问) │
│ ▲ │
│ │ │
│ Extension ClassLoader │
│ (加载 JDK 扩展包) │
│ ▲ │
│ │ │
│ Application ClassLoader │
│ (加载 classpath 中的类) │
│ ▲ │
│ │ │
│ 自定义 ClassLoader │
│ │
│ 加载请求向上传递,父类无法加载才由子类加载 │
│ │
└─────────────────────────────────────────────────────────────────┘好处:
- 安全性:核心类(如 java.lang.String)由 Bootstrap 加载,不会被替换
- 避免重复加载:父类已加载的类不会被子类重复加载
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.hprofQ16: 如何查看 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.logGC 日志解读:
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
原因:线程太多
解决:
- 减少线程数
- 减小堆内存(线程占用栈空间)
- 减小 -XssQ19: 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?
原则:
- 先分析,再调优
- 避免过早优化
- 监控是优化的前提
常用优化方向:
| 场景 | 优化方案 |
|---|---|
| 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 参数调优 | ⭐⭐⭐⭐ |