JVM 调优面试全攻略

内存模型 · GC算法 · 垃圾收集器 · 调优参数 · OOM排查 · 类加载

内存区域 GC算法 垃圾收集器 OOM排查 类加载机制
📚 想要深入理解原理?推荐搭配专题图解学习

本页侧重面试考点速答。若需理解底层原理和运行机制,请看以下专题图解:

📘 JVM内存模型图解

JVM 内存模型(运行时数据区)

5 大区域一览

堆 Heap 对象实例、数组
GC 主战场
线程共享
方法区 / 元空间 类信息、常量、静态变量
JDK8后用本地内存
线程共享
虚拟机栈 栈帧(局部变量表、操作数栈)
每个方法一个栈帧
线程私有
本地方法栈 Native 方法服务
C/C++ 实现
线程私有
程序计数器 PC 当前线程执行的字节码行号 | 唯一不会 OOM 的区域 | 线程私有

JVM 内存 = 房间空间:堆=操作台(大家共用,做大物件)| 方法区=配方手册(共享知识)| 栈=每个员工的私人工具箱(各用各的)| PC=每个员工做到哪一步的标记

堆的详细结构(面试重点)

新生代 Young(1/3)
Eden : S0 : S1 = 8:1:1
老年代 Old(2/3)
长期存活的对象
Eden 区
新对象分配(80%)
S0/S1
复制交换
Old
年龄≥15 或大对象

对象晋升老年代的条件:

• 年龄达到阈值(默认 -XX:MaxTenuringThreshold=15
• Survivor 区空间不足,动态年龄判断
• 大对象直接进入老年代(-XX:PretenureSizeThreshold

方法区 / 元空间的变化

版本实现位置OOM 风险
JDK 7 及之前 永久代(PermGen) JVM 堆中 java.lang.OutOfMemoryError: PermGen space
JDK 8+ 元空间(Metaspace) 本地内存 java.lang.OutOfMemoryError: Metaspace

为什么要改?永久代大小固定容易 OOM,元空间使用本地内存,自动扩展(但需要 -XX:MaxMetaspaceSize 限制)。

面试必考:哪些区域会 OOM?堆(OOM)、方法区/元空间(OOM)、栈(StackOverflowError / OOM)、PC(不会 OOM)。字符串常量池 JDK7 后从永久代移到了堆中。

GC 垃圾回收算法

4 大基础算法

算法原理优点缺点应用
标记-清除 标记存活→清除未标记 简单 内存碎片多 老年代 CMS
标记-复制 存活对象复制到另一半,清空当前 无碎片,效率高 空间减半 新生代 Serial/ParNew
标记-整理 标记→存活对象向一端移动→清理边界 无碎片 移动开销大 老年代 Serial Old/Parallel Old
分代收集 新生代用复制,老年代用标记-清除/整理 综合最优 实现复杂 现代 JVM 通用

标记-清除 = 打扫卫生,看到垃圾就捡,但地上到处是小空位(碎片);标记-复制 = 把有用的东西搬到隔壁房间,然后这间全部重置(简单但不省空间);标记-整理 = 把有用的东西全部推到一边,剩下全是空地(整齐但累)

GC 类型

类型触发条件回收区域STW
Minor GC / Young GC Eden 区满 新生代 短暂 STW(毫秒级)
Major GC / Old GC 老年代满 老年代 STW 较长
Full GC 整个堆+方法区满 / 调用 System.gc() 新生代+老年代+元空间 STW 最长,要避免!

对象存活判断:可达性分析

GC Roots
起点对象
引用链遍历
深度优先搜索
可达 = 存活
保留
+
不可达 = 回收
但还可能自救

GC Roots 包括(面试要背全):

GC Root具体是什么为什么是 Root泄漏场景
① 栈中局部变量方法里正在用的对象:User u = userService.get(id)方法正在执行,对象肯定还在用方法执行完自动出栈,一般不泄漏
② 静态变量static List<User> cachestatic Map类不卸载,static 就一直在最常见!只 put 不 remove → 无限增长 → OOM
③ 活跃线程线程本身就是 Root,线程内部所有引用都可达线程没终止就不会被回收ThreadLocal 没 remove + 线程池复用 → 串数据 + 泄漏
④ 常量引用方法区中的常量:static final String X = new String("x")常量永不卸载较少见
⑤ 同步锁synchronized(obj) 持有的对象锁还在持有,对象不能消失锁没释放 → 关联对象不回收
⑥ JNI 引用本地方法(C/C++)通过 JNI 持有的 Java 对象native 代码还在用native 代码忘记释放
⑦ JVM 内部基本类型的 Class 对象、异常对象、类加载器JVM 运行需要它们不泄漏(JVM 管)
⚠️ 面试一句话总结

GC Roots 就是 "JVM 认为一定还活着的东西":正在用的变量、static 变量、活跃的线程、锁持有的对象。内存泄漏的本质 = 对象不用了,但还被某个 GC Root 引用着,GC 走不到"该回收"的结论。

🤔 普通变量会泄漏吗?

会,但很容易被 GC 自动回收。关键在于"引用链是否还连着 GC Root":

变量类型连着 GC Root 吗泄漏概率原因
方法内局部变量
User u = new User()
方法执行期间连着
方法结束 → 自动断开
极低 方法结束 → 栈帧弹出 → u 不再是 Root 引用 → User 对象不可达 → GC 回收
实例变量(成员变量)
private List<User> list
连着 → 只要外层对象还活着 外层对象被回收 → 成员变量跟着回收。但如果外层对象本身泄漏了,里面的成员也跟着泄漏
static 变量
static Map cache
永远连着(类不卸载就在) 极高! static 是永久 Root,只 put 不 remove → 永远不可达不了 → 永远不回收
ThreadLocal 连着活跃线程(线程池不销毁) 极高! 线程池复用线程 → ThreadLocalMap 跟着线程不死 → value 强引用不回收
集合里的元素
List / Map 里的对象
取决于集合本身是否连着 Root 看集合 集合是局部变量 → 方法结束自动回收。集合是 static → 里面的元素永远不回收
核心规律:变量本身不会泄漏,泄漏的是"变量引用的对象连着 GC Root 断不掉"
• 普通局部变量 → 方法结束自动断 → 安全
• static / ThreadLocal → 永久连着 Root → 必须手动 remove 才能断
• 集合元素 → 跟着集合走,集合是 static 则元素也永久不回收

4 种引用类型

引用类型回收时机用途
强引用(Object obj = new Object()) 永远不回收(只要引用还在) 普通对象
软引用(SoftReference) 内存不足时才回收 缓存(如图片缓存)
弱引用(WeakReference) 下一次 GC 就回收 ThreadLocal 的 key、WeakHashMap
虚引用(PhantomReference) 随时可回收,get()永远返回null 跟踪 GC 回收、管理堆外内存

7 大垃圾收集器

收集器组合与关系

Serial
单线程 新生代
+
Serial Old
单线程 老年代
ParNew
多线程 新生代
+
CMS
并发 低延迟 老年代
Parallel Scavenge
吞吐量优先 新生代
+
Parallel Old
吞吐量优先 老年代
G1
分Region 停顿可控 JDK9默认
+
ZGC
超低延迟 亚毫秒级 JDK15+

各收集器对比

收集器算法特点适用场景
Serial 复制 单线程,STW 客户端模式、小内存
ParNew 复制 多线程版 Serial 配合 CMS
Parallel Scavenge 复制 吞吐量优先,自适应调节 后台计算型应用
CMS 标记-清除 并发标记清除,低延迟,有碎片 JDK9 已废弃,追求响应
G1 复制+整理 Region 分区,可预测停顿 JDK9+ 默认,大堆内存
ZGC 复制+整理 亚毫秒级停顿,着色指针 JDK15+,超低延迟场景

CMS 四个阶段(面试高频)

1. 初始标记
STW(极短)
2. 并发标记
与用户线程并行
3. 重新标记
STW(修正)
4. 并发清除
与用户线程并行

CMS 三大问题:

① CPU 敏感(并发阶段占 CPU)
② 浮动垃圾(并发清除阶段新产生的垃圾,下次才能清)
③ 内存碎片(标记-清除 算法固有问题)

G1 核心机制

• 堆分为多个等大 Region(1~32MB),每个 Region 可以是 Eden/Survivor/Old/Humongous

• 维护优先级列表,优先回收价值最大的 Region

• 可设定最大停顿时间:-XX:MaxGCPauseMillis=200

• 回收步骤:初始标记(STW) → 并发标记 → 最终标记(STW) → 筛选回收(STW)

ZGC 创新点

着色指针:在指针中存储 GC 信息,不需要额外的标记位

读屏障:对象移动时通过读屏障修正引用,实现并发整理

• 停顿时间 < 1ms,且不随堆大小增长

• JDK 21 引入 分代 ZGC,进一步提升吞吐

JVM 调优参数速查

内存参数

参数含义推荐值
-Xms 初始堆大小 与 Xmx 相同,避免动态扩容
-Xmx 最大堆大小 物理内存的 60~80%
-Xmn / -XX:NewRatio 新生代大小 堆的 1/3~1/2
-XX:SurvivorRatio Eden:S0:S1 比例 8(默认 8:1:1)
-XX:MetaspaceSize / MaxMetaspaceSize 元空间大小 256M~512M
-Xss 线程栈大小 512K~1M(默认 1M)

GC 参数

参数含义
-XX:+UseSerialGC 使用 Serial + Serial Old
-XX:+UseParNewGC 使用 ParNew + Serial Old
-XX:+UseParallelGC 使用 Parallel Scavenge + Parallel Old(JDK8 默认)
-XX:+UseConcMarkSweepGC 使用 ParNew + CMS
-XX:+UseG1GC 使用 G1(JDK9+ 默认)
-XX:+UseZGC 使用 ZGC(JDK15+)
-XX:MaxGCPauseMillis G1 目标最大停顿时间(默认 200ms)
-XX:+PrintGCDetails 打印 GC 详细日志(JDK8)
-Xlog:gc* 打印 GC 日志(JDK9+ 统一日志格式)

生产环境典型配置

# JDK8 常见配置
java -Xms4g -Xmx4g -Xmn2g \
  -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
  -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
  -Xloggc:/var/log/gc.log \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/heapdump.hprof \
  -jar app.jar

# JDK17+ 常见配置
java -Xms4g -Xmx4g \
  -XX:+UseZGC \
  -Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags \
  -XX:+HeapDumpOnOutOfMemoryError \
  -jar app.jar

面试常说:-Xms 和 -Xmx 设成一样,避免堆大小动态调整的开销。HeapDumpOnOutOfMemoryError 一定要加,OOM 时自动生成堆转储,排查利器。

OOM 排查实战

OOM 类型速查

错误类型原因排查方向
Java heap space 堆内存不足 内存泄漏 / 堆太小 / 大对象
Metaspace 元空间不足 动态代理类过多 / 没限 MaxMetaspaceSize
GC overhead limit exceeded GC 回收的不到 2% 堆快满了,GC 疯狂跑但没用
unable to create new native thread 线程数太多 线程泄漏 / OS 限制
Direct buffer memory 堆外内存不足 NIO ByteBuffer / Netty 未释放

排查四步法

1. 看日志
OOM 错误类型 + 堆栈
2. 看监控
GC 频率/耗时/内存曲线
3. Dump 分析
jmap/MAT/Arthas
4. 定位代码
找到泄漏对象+引用链

常用排查命令

# 查看 JVM 进程
jps -l

# 查看堆内存概览
jmap -heap <pid>

# 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>

# 查看对象统计(直方图)
jmap -histo <pid> | head -20

# 查看 GC 统计
jstat -gcutil <pid> 1000 10

# 查看线程栈
jstack <pid> > thread_dump.txt

# Arthas 在线诊断(推荐)
dashboard # 综合面板
heapdump /tmp/heap.hprof # 导出堆
thread -n 3 # 最忙的3个线程

内存泄漏常见场景

ThreadLocal 未 remove(线程池复用线程)
静态集合持续增长(static List/Map 只加不删)
资源未关闭(Connection/Stream/Channel)
监听器/回调未注销(EventBus 只注册不反注册)
缓存无过期(自己写的 Map 缓存没 LRU/TTL)

类加载机制

类加载 5 个阶段

加载
读 .class 文件
验证
格式/元数据/字节码
准备
静态变量赋零值
解析
符号引用→直接引用
初始化
clinit() 执行

关键区分:

准备阶段:static int a = 10; → a 先赋 0(零值),不是 10!
初始化阶段:a 才被赋值为 10
static final 常量在准备阶段就赋值(编译期常量)

双亲委派模型

Bootstrap ClassLoader
rt.jar / 核心类库
↑ 委派
Extension ClassLoader
ext 目录 / JDK9+ Platform ClassLoader
↑ 委派
Application ClassLoader
classpath / 应用类
↑ 委派
自定义 ClassLoader
Tomcat / 热部署 / 隔离加载

核心原则:先让父加载器加载,父加载器搞不定才自己加载。保证核心类(如 java.lang.Object)不会被自定义类覆盖。

打破双亲委派的场景

Tomcat:每个 WebApp 独立 ClassLoader,先自己加载再委派(保证应用隔离)
SPI 机制:JDBC 的 Driver 接口在 rt.jar,实现在第三方 jar,Bootstrap CL 加不到 → 用 Thread Context ClassLoader
OSGi:模块化加载,网状结构而非树状
热部署:重新创建 ClassLoader 加载新版本类

面试中说出"双亲委派保证了核心类的安全性"还不够,要加上"SPI 用线程上下文类加载器打破双亲委派"和"Tomcat 自定义 ClassLoader 实现应用隔离"这两个实际场景。

高频面试问答 TOP 15

1JVM 内存模型有哪些区域?

5 大区域:
(对象实例,线程共享)
方法区/元空间(类信息、常量,线程共享)
虚拟机栈(栈帧,线程私有)
本地方法栈(Native 方法,线程私有)
程序计数器(字节码行号,线程私有,唯一不会 OOM)

2堆和栈的区别?

:线程私有,存局部变量/方法调用,自动分配释放,空间小但快
:线程共享,存对象实例,GC 管理,空间大但相对慢
• 栈溢出 → StackOverflowError;堆溢出 → OutOfMemoryError

3什么时候会触发 Full GC?

① 老年代空间不足
② 方法区/元空间空间不足
③ 显式调用 System.gc()
④ Minor GC 后老年代空间不够放存活对象
⑤ CMS GC 时出现 Concurrent Mode Failure

4G1 和 CMS 的区别?

CMS:标记-清除,有碎片,不能处理浮动垃圾,JDK9 废弃
G1:Region 分区 + 复制整理,无碎片,可预测停顿时间,JDK9 默认
• G1 适合大堆(6G+),CMS 适合中小堆
• G1 回收是选择价值最大的 Region 组,不是整代回收

5什么是 STW?为什么不能完全避免?

STW = Stop The World,GC 时暂停所有用户线程。
不能完全避免,因为:GC 需要确保一致性快照,如果用户线程同时修改对象引用,GC 结果就不准确。CMS/G1/ZGC 都在尽量缩短 STW 时间,ZGC 已做到亚毫秒级。

6什么是内存泄漏?和内存溢出的区别?

内存泄漏:对象不再使用但 GC 无法回收(如静态集合持有引用)→ 是原因
内存溢出 OOM:没有足够内存分配新对象 → 是结果
• 内存泄漏积累到一定程度 → 内存溢出

7JVM 调优的目标是什么?

两个核心目标(互斥,需权衡):
低延迟:减少 GC 停顿时间(选 CMS/G1/ZGC)
高吞吐:减少 GC 总耗时占比(选 Parallel Scavenge)
实际项目中一般是降低延迟 + 避免 Full GC。

8新生代为什么用复制算法?

新生代对象 98% 都是朝生夕灭,存活率极低。
复制算法只需要复制少量存活对象到 Survivor,清空 Eden 即可,效率极高。
而且 8:1:1 的比例让空间浪费只有 10%,非常划算。

9什么是逃逸分析?有什么用?

分析对象的作用域是否"逃逸"出方法。
没有逃逸的对象可以:
栈上分配:不用在堆上分配,方法结束自动回收
标量替换:拆成基本类型,直接在栈上
消除同步锁:对象不出方法,没有并发问题,锁可消除

10什么情况下对象会进入老年代?

① 年龄达到 MaxTenuringThreshold(默认 15)
② Survivor 区相同年龄对象大小超过 Survivor 空间一半(动态年龄判断)
大对象直接进入(-XX:PretenureSizeThreshold)
④ Minor GC 后 Survivor 放不下

11如何判断一个对象可以被回收?

两种算法:
引用计数法:有引用+1,无引用-1,为0就回收。问题:循环引用无法回收。
可达性分析法(JVM 实际使用):从 GC Roots 出发,沿引用链遍历,不可达的对象可回收。

12类加载的双亲委派模型有什么好处?

安全性:防止核心类被篡改(如自定义 java.lang.Object 不会被加载)
避免重复加载:父加载器已加载的类,子加载器不需要再加载
层次清晰:每一层负责自己的类,职责分明

13Java 有哪几种引用?分别什么场景用?

强引用:永远不会被回收(OutOfMemoryError 也不回收)
软引用:内存不足时回收 → 缓存
弱引用:下一次 GC 回收 → ThreadLocal、WeakHashMap
虚引用:随时回收,get() 返回 null → 跟踪 GC、管理堆外内存

14线上 OOM 怎么排查?

① 看错误日志,确认 OOM 类型
② 用 jmap -histo 看哪些对象最多
③ 用 jmap -dump 导出堆转储(或提前开启 HeapDumpOnOutOfMemoryError)
④ 用 MAT 分析堆转储,找到 GC Roots 引用链
⑤ 或用 Arthas 在线诊断:dashboard 看 memory,heapdump 导出
⑥ 定位代码行,修复泄漏

15永久代和元空间的区别?

永久代(JDK7-):在 JVM 堆中,大小固定,容易 OOM
元空间(JDK8+):在本地内存中,自动扩展,不容易 OOM
• 常量池从永久代移到堆中(JDK7 开始)
• 字符串常量池在堆,运行时常量池在元空间

速记卡片(面试前 5 分钟看)

内存区域速记:

堆(共享) | 方法区(共享) | 栈(私有) | 本地栈(私有) | PC(私有,不OOM)

堆结构速记:

新生代1/3(Eden:S0:S1=8:1:1) | 老年代2/3 | 年龄≥15晋升

GC算法速记:

标记清除(碎片) | 标记复制(新生代) | 标记整理(老年代) | 分代收集(综合)

收集器速记:

Serial→ParNew→Parallel→CMS→G1→ZGC | JDK8默认Parallel | JDK9+默认G1

调优参数速记:

Xms=Xmx | HeapDumpOnOOM | MaxGCPauseMillis | UseG1GC/ZGC

类加载速记:

加载→验证→准备(零值)→解析→初始化 | 双亲委派 | SPI打破

引用速记:

强(不回收) | 软(内存不足) | 弱(下次GC) | 虚(跟踪回收)

OOM排查速记:

jps→jmap -histo→jmap -dump→MAT分析→定位代码

面试官追问链(真实面试路径)

链路 1:内存模型 → 堆结构 → GC

"JVM 内存有哪些区域?" → "堆怎么划分?" → "对象什么时候进老年代?" → "什么触发 Full GC?"

链路 2:GC 算法 → 收集器 → 调优

"有哪些 GC 算法?" → "G1 和 CMS 区别?" → "线上怎么选收集器?" → "JVM 调优参数有哪些?"

链路 3:OOM → 排查 → 修复

"OOM 有哪些类型?" → "线上 OOM 怎么排查?" → "常见内存泄漏场景?" → "怎么避免?"

链路 4:类加载 → 双亲委派 → 打破

"类加载过程?" → "双亲委派是什么?" → "有什么好处?" → "哪些场景打破了双亲委派?"

JVM调优三原则速记

JVM调优:① 先监控后调优(没有数据不调参)→ ② 堆分区比参数值更重要(Eden:Survivor比例影响GC频率)→ ③ 生产环境优先选G1/ZGC(低延迟优于吞吐量)