定位:JVM 面试实战篇------不是背命令,而是从"用户反馈卡顿"这个唯一入口出发,走完所有分支的完整决策链
读法:每张流程图都是"现象 → 命令 → 判断 → 调整"的闭环,任何一环拿掉,后面的结论都站不住
一、总纲:只有一个分水岭
用户说"卡",本质只有两种可能:
- CPU 高 = 有人在疯狂干活(业务线程空转,或 GC 线程被迫加班)→ 要回答"谁在干、干什么"
- CPU 低 = 有人在等 (等锁 / 等下游 / 等池子)或反复被打断(STW 停顿)→ 要回答"在等什么"
⚠️ 分水岭的正确读法:top 看 load ÷ 核数 ≈ 1 才算满载 (不是 load≈1,那是单核机的标准);同时看 us%(用户态) 和 sy%(内核态)------load 里混着 IO 等待的线程,不能单看 load 下结论。
#mermaid-svg-b9qIt7XtnZb9gH3I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b9qIt7XtnZb9gH3I .error-icon{fill:#552222;}#mermaid-svg-b9qIt7XtnZb9gH3I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b9qIt7XtnZb9gH3I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b9qIt7XtnZb9gH3I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b9qIt7XtnZb9gH3I .marker.cross{stroke:#333333;}#mermaid-svg-b9qIt7XtnZb9gH3I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b9qIt7XtnZb9gH3I p{margin:0;}#mermaid-svg-b9qIt7XtnZb9gH3I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I .cluster-label text{fill:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I .cluster-label span{color:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I .cluster-label span p{background-color:transparent;}#mermaid-svg-b9qIt7XtnZb9gH3I .label text,#mermaid-svg-b9qIt7XtnZb9gH3I span{fill:#333;color:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I .node rect,#mermaid-svg-b9qIt7XtnZb9gH3I .node circle,#mermaid-svg-b9qIt7XtnZb9gH3I .node ellipse,#mermaid-svg-b9qIt7XtnZb9gH3I .node polygon,#mermaid-svg-b9qIt7XtnZb9gH3I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b9qIt7XtnZb9gH3I .rough-node .label text,#mermaid-svg-b9qIt7XtnZb9gH3I .node .label text,#mermaid-svg-b9qIt7XtnZb9gH3I .image-shape .label,#mermaid-svg-b9qIt7XtnZb9gH3I .icon-shape .label{text-anchor:middle;}#mermaid-svg-b9qIt7XtnZb9gH3I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b9qIt7XtnZb9gH3I .rough-node .label,#mermaid-svg-b9qIt7XtnZb9gH3I .node .label,#mermaid-svg-b9qIt7XtnZb9gH3I .image-shape .label,#mermaid-svg-b9qIt7XtnZb9gH3I .icon-shape .label{text-align:center;}#mermaid-svg-b9qIt7XtnZb9gH3I .node.clickable{cursor:pointer;}#mermaid-svg-b9qIt7XtnZb9gH3I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b9qIt7XtnZb9gH3I .arrowheadPath{fill:#333333;}#mermaid-svg-b9qIt7XtnZb9gH3I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b9qIt7XtnZb9gH3I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b9qIt7XtnZb9gH3I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b9qIt7XtnZb9gH3I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b9qIt7XtnZb9gH3I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b9qIt7XtnZb9gH3I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b9qIt7XtnZb9gH3I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b9qIt7XtnZb9gH3I .cluster text{fill:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I .cluster span{color:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b9qIt7XtnZb9gH3I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b9qIt7XtnZb9gH3I rect.text{fill:none;stroke-width:0;}#mermaid-svg-b9qIt7XtnZb9gH3I .icon-shape,#mermaid-svg-b9qIt7XtnZb9gH3I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b9qIt7XtnZb9gH3I .icon-shape p,#mermaid-svg-b9qIt7XtnZb9gH3I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b9qIt7XtnZb9gH3I .icon-shape .label rect,#mermaid-svg-b9qIt7XtnZb9gH3I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b9qIt7XtnZb9gH3I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b9qIt7XtnZb9gH3I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b9qIt7XtnZb9gH3I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CPU 飙高
us% 高
业务线程
GC 线程
(GC Thread/VM Thread/G1 Conc)
sy% 高 us% 不高
CPU 不高
BLOCKED
RUNNABLE 卡 socketRead
(CPU低却RUNNABLE=最大陷阱)
WAITING / TIMED_WAITING
状态都正常, 但就是卡
功能完全卡死
用户反馈: 系统卡
top: 看 load/核数 + us% + sy%
CPU 高不高?
场景A: 有人在干活
top -Hp PID 找吃CPU的线程
吃CPU的是谁?
printf '%x' tid 转16进制
jstack PID 查 nid
→ 场景A1: 死循环/重计算
jstat -gc PID 1000 10
两次采样算差值
→ 场景A2: GC 被迫加班
vmstat 看 cs 上下文切换
→ 场景A3: 线程太多/系统调用密集
场景B: 有人在等 / 被打断
jstack PID 看线程状态
大量线程什么状态?
等同一把锁
→ 场景B1: 锁竞争
→ 场景B2: 下游慢(DB/接口)
不是 JVM 的锅
卡在 pool.getConnection / queue.take
→ 场景B3: 池子耗尽
jstat 两次采样看 GC 增量
→ 场景B4: STW 停顿型卡顿
jstack 拉到最后
Found one Java-level deadlock
→ 场景B5: 死锁
二、场景 A:CPU 飙高(有人在干活)
A1 业务线程死循环 / 重计算
| 项 | 内容 |
|---|---|
| 现象 | top 中 us% 高;top -Hp <PID> 看到某几个业务线程独占 90%+ |
| 命令三板斧 | top -Hp <PID> 找 tid → printf '%x' <tid> 转 16 进制 → `jstack |
| 判断依据 | 栈顶停在自家代码的循环 / 正则回溯 / 大对象序列化;连抓 3 次 jstack 栈顶都在同一行 = 基本坐实 |
| 调整方案 | 修代码:循环跳出条件、算法复杂度(O(n²)→O(n))、批处理拆小;临时兜底 = 重启 + 限流 |
A2 GC 线程被迫加班
| 项 | 内容 |
|---|---|
| 现象 | top -H 里 GC Thread#0、VM Thread、G1 Conc#0 排最前 |
| 命令 | jstat -gc <PID> 1000 10------取其中两次采样算差值,看 YGC/FGC 次数和耗时涨多快 |
| 判断依据 | YGC 一秒好几次、YGCT 猛涨 = 对象产生速率 > 回收速率,GC 线程只能满负荷追着跑,业务线程抢不到时间片 |
| 因果链 | 对象造得太快 → 年轻代秒满 → YGC 高频 → 晋升老年代也快 → FGC 跟上 → GC 线程吃满 CPU |
| 调整方案 | ① 减少对象创建(循环里 new 大对象、字符串 + 拼接、装箱拆箱)② 调大年轻代(-Xmn)让对象死在年轻代 ③ 换 G1 ④ 兜底:重启 + 限流 |
追问深挖①:对象到底是怎么"激增"的------以字符串拼接为例
常见误解:"JDK7 之后字符串常量池挪到堆里了,"a" + "b" 拼接不就是在常量池里找、找不到才建吗?那拼接造不出多少对象吧?"------只对了一半,漏了主犯 。因果链分叉在 javac:
1、 String str = "a" + "b";("a"和"b"都是编译期就确定的字面量)→ javac 常量折叠 ,编译期直接合成 "ab" 进常量池,运行时零对象创建------"找不到才建",字节码等价于:String str = "ab";
特点:
plaintext
1. 运行时不会创建任何堆上对象;
2. 直接将"ab"放入字符串常量池;
3. 逻辑:运行时去常量池查找,存在就复用引用,不存在则在常量池创建。
2、 str1 + str2(含变量)→ javac 改走另一条路:编译成 new StringBuilder().append().append().toString()------toString() 每次 new 一个全新的堆 String,且不进常量池 (除非手动 intern());只要拼接表达式中存在变量,编译期无法预知运行时真实字符串内容,就无法做常量折叠。
代码示例:
java
String a = "a";
String b = "b";
String str = a + b;
javac 编译之后,等价伪代码:
java
String str = new StringBuilder().append(a).append(b).toString();
跟进StringBuilder#toString()JDK8 源码:
java
public String toString() {
return new String(value, 0, count);
}
所以这里会new出来一个全新的普通堆 String 对象 。
特点:
plaintext
1. new StringBuilder():堆上新 StringBuilder 实例;
2. StringBuilder 内部的字符数组char[](JDK9 + 改为 byte []);
3. toString():new String(),生成最终字符串对象。
3、 所以循环里 result += item = 每轮造出 StringBuilder + 内部数组 + 新 String,一万次循环 = 数万个朝生夕死的对象 → 年轻代被快速灌满 → GC 被迫加班。
坏代码示例(生产高频踩坑)
java
String result = "";
List<String> dataList = getBizData(); // 一万条业务数据
for (String item : dataList) {
result = result + item;
}
很多开发者以为编译器会自动优化为一个 StringBuilder 复用。真相:每一轮循环,都会完整执行一次new StringBuilder()、append、toString。
每一次循环等价执行:
java
result = new StringBuilder().append(result).append(item).toString();
每一轮循环产生对象:
- 1 个 StringBuilder 对象
- StringBuilder 内部字符数组
- 1 个新的 String 对象
循环一万次,就会产生数万个短命小对象。上一轮循环产生的 String、StringBuilder,循环结束后没有引用,马上变成垃圾,全部往 Eden 新生代堆里塞。所以常量折叠救不了变量拼接,循环里
+=字符串依然是经典垃圾制造机;循环外提一个StringBuilder,从对象产生的源头掐断,比任何 GC 调参都治本。
追问深挖②:GC 线程飙 CPU 只是一瞬间,top 快照抓得住吗
可以抓得住,原因:
- 单次 YGC 几十 ms,top 默认 3 秒一刷,确实可能漏看------这一半是对的;
- 但凡是让用户感知到卡顿的 GC 问题,病根必然是"频繁"而非"单次"(单次几十 ms 的停顿用户根本感觉不到);
- 频繁意味着 GC 线程持续霸榜,每次快照都在前列------所以 top 抓得住;
- 想更稳就看
TIME+累计列:隔 10 秒看两次,GC 线程累计 CPU 时间涨得飞快 = 在加班,比瞬时百分比可靠; - 但采样终究是赌运气,实锤要换不依赖采样的工具 :jstat 两次采样算差值(累计计数器不存在"错过")+ GC 日志(
-Xlog:gc*全程逐条记录)。
口诀:top 负责"现在谁热",jstat 差值和 GC 日志负责"这段时间谁干得多"------后者才是实锤。
A3 sy% 高(内核态忙)
| 项 | 内容 |
|---|---|
| 现象 | us% 不高但 sy% 高 |
| 判断依据 | vmstat 1 看 cs(上下文切换)飙高 = 线程太多在互相切换;或系统调用/IO 密集 |
| 调整方案 | 砍线程数(线程池不是越大越好)、IO 批量化、减少锁竞争带来的 park/unpark |
三、场景 B:CPU 不高(有人在等,或被打断)
#mermaid-svg-AvrlvAcNNQzupHFT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AvrlvAcNNQzupHFT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AvrlvAcNNQzupHFT .error-icon{fill:#552222;}#mermaid-svg-AvrlvAcNNQzupHFT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AvrlvAcNNQzupHFT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AvrlvAcNNQzupHFT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AvrlvAcNNQzupHFT .marker.cross{stroke:#333333;}#mermaid-svg-AvrlvAcNNQzupHFT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AvrlvAcNNQzupHFT p{margin:0;}#mermaid-svg-AvrlvAcNNQzupHFT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AvrlvAcNNQzupHFT .cluster-label text{fill:#333;}#mermaid-svg-AvrlvAcNNQzupHFT .cluster-label span{color:#333;}#mermaid-svg-AvrlvAcNNQzupHFT .cluster-label span p{background-color:transparent;}#mermaid-svg-AvrlvAcNNQzupHFT .label text,#mermaid-svg-AvrlvAcNNQzupHFT span{fill:#333;color:#333;}#mermaid-svg-AvrlvAcNNQzupHFT .node rect,#mermaid-svg-AvrlvAcNNQzupHFT .node circle,#mermaid-svg-AvrlvAcNNQzupHFT .node ellipse,#mermaid-svg-AvrlvAcNNQzupHFT .node polygon,#mermaid-svg-AvrlvAcNNQzupHFT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AvrlvAcNNQzupHFT .rough-node .label text,#mermaid-svg-AvrlvAcNNQzupHFT .node .label text,#mermaid-svg-AvrlvAcNNQzupHFT .image-shape .label,#mermaid-svg-AvrlvAcNNQzupHFT .icon-shape .label{text-anchor:middle;}#mermaid-svg-AvrlvAcNNQzupHFT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AvrlvAcNNQzupHFT .rough-node .label,#mermaid-svg-AvrlvAcNNQzupHFT .node .label,#mermaid-svg-AvrlvAcNNQzupHFT .image-shape .label,#mermaid-svg-AvrlvAcNNQzupHFT .icon-shape .label{text-align:center;}#mermaid-svg-AvrlvAcNNQzupHFT .node.clickable{cursor:pointer;}#mermaid-svg-AvrlvAcNNQzupHFT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AvrlvAcNNQzupHFT .arrowheadPath{fill:#333333;}#mermaid-svg-AvrlvAcNNQzupHFT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AvrlvAcNNQzupHFT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AvrlvAcNNQzupHFT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AvrlvAcNNQzupHFT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AvrlvAcNNQzupHFT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AvrlvAcNNQzupHFT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AvrlvAcNNQzupHFT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AvrlvAcNNQzupHFT .cluster text{fill:#333;}#mermaid-svg-AvrlvAcNNQzupHFT .cluster span{color:#333;}#mermaid-svg-AvrlvAcNNQzupHFT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AvrlvAcNNQzupHFT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AvrlvAcNNQzupHFT rect.text{fill:none;stroke-width:0;}#mermaid-svg-AvrlvAcNNQzupHFT .icon-shape,#mermaid-svg-AvrlvAcNNQzupHFT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AvrlvAcNNQzupHFT .icon-shape p,#mermaid-svg-AvrlvAcNNQzupHFT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AvrlvAcNNQzupHFT .icon-shape .label rect,#mermaid-svg-AvrlvAcNNQzupHFT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AvrlvAcNNQzupHFT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AvrlvAcNNQzupHFT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AvrlvAcNNQzupHFT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 大量 BLOCKED
锁里做慢操作(IO/DB)
锁粒度过大
大量 RUNNABLE
栈顶 socketRead
大量 WAITING /
TIMED_WAITING
状态健康但就是卡
CPU 不高 + 卡顿
jstack PID 看状态
线程状态分布?
B1 锁竞争
waiting to lock <0xAAA>
查 <0xAAA> 被谁 locked
持锁线程在干嘛?
调整: 锁里不做 IO
缩小同步块粒度
调整: 读写锁拆分 /
ConcurrentHashMap /
分片锁(1把锁拆16把)
B2 下游慢
陷阱: 网络IO阻塞也显示 RUNNABLE
CPU 低 + RUNNABLE = 等下游, 不是在算
调整: 必设 connect/read 超时
熔断降级(Sentinel)
查慢 SQL / 下游扩容
这不是 JVM 问题, 别瞎调 JVM
B3 池子耗尽
卡在 DB 连接池 getConnection
或线程池 queue.take
调整: 先查连接泄漏(没 close)
再查慢 SQL 拖住连接不还
最后才是调大池子
B4 STW 停顿型卡顿
jstat 两次采样: FGC 间隔很短
接口毛刺 = 反复被打断
进 GC 深挖流程(图三)
B1~B3 一句话区分
- BLOCKED = 在等锁 (
synchronized进不去)→ 找持锁者 - RUNNABLE 卡 socketRead = 在等下游(DB / HTTP 接口)→ JVM 线程状态不会为网络等待单独标记,这是最大的误判点
- WAITING/TIMED_WAITING = 在等条件 (池子里的资源、
condition.await())→ 查池子
B5 死锁(功能卡死型)
| 项 | 内容 |
|---|---|
| 现象 | 某功能完全无响应,CPU 反而很低 |
| 命令 | jstack <PID> 直接拉到最后 :JVM 自动汇总 Found one Java-level deadlock,列出每个线程持有哪把锁、在等哪把锁、卡在哪一行 |
| 调整方案 | 统一全局持锁顺序(都按 obj1→obj2 申请)/ 用 tryLock 加超时 / 消除嵌套锁 |
四、场景 B4 深挖:STW 停顿型卡顿(GC 线全流程)
这是链条最长、面试追问最多的分支,单独一张图:
判断标尺(A2/B4 通用):GC 多长算长、多频算高
阈值不是背出来的,是推出来的。因果链:
- GC 停顿时间会原样加到接口延迟上(STW 期间所有业务线程冻结);
- 所以"多长算长"的锚点 = 业务能忍的最大停顿,用你的 P99 延迟预算倒推------P99 要求 200ms,单次 GC 停顿就不该碰 100ms 这条线;
- "多频算高"的锚点 = 停顿占空比:GC 总耗时占运行时间应 < 2%~5%,超过就是 GC 在挤占业务。
经验参考线(以 P99 ≈ 200ms (100个请求中只有1个请求大于等于200ms)的普通后台系统为例):
| 健康 | 要关注 | 病态 | |
|---|---|---|---|
| YGC 单次 | 几 ms ~ 几十 ms | >100ms | >200ms |
| YGC 频率 | 几秒~几十秒一次 | 1 秒一次 | 一秒好几次 |
| FGC 频率 | 几天一次甚至没有 | 小时级 | 分钟/秒级 |
| FGC 单次(ParallelOld) | 几百 ms | >1s | 秒级以上 |
特例:CMS 并发失败退化成的 Serial Old FGC 是单线程清堆,大堆能停几十秒------出现即事故,不适用上表。
#mermaid-svg-V7IjxC6DcDiV0yVM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V7IjxC6DcDiV0yVM .error-icon{fill:#552222;}#mermaid-svg-V7IjxC6DcDiV0yVM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V7IjxC6DcDiV0yVM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V7IjxC6DcDiV0yVM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V7IjxC6DcDiV0yVM .marker.cross{stroke:#333333;}#mermaid-svg-V7IjxC6DcDiV0yVM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V7IjxC6DcDiV0yVM p{margin:0;}#mermaid-svg-V7IjxC6DcDiV0yVM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM .cluster-label text{fill:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM .cluster-label span{color:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM .cluster-label span p{background-color:transparent;}#mermaid-svg-V7IjxC6DcDiV0yVM .label text,#mermaid-svg-V7IjxC6DcDiV0yVM span{fill:#333;color:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM .node rect,#mermaid-svg-V7IjxC6DcDiV0yVM .node circle,#mermaid-svg-V7IjxC6DcDiV0yVM .node ellipse,#mermaid-svg-V7IjxC6DcDiV0yVM .node polygon,#mermaid-svg-V7IjxC6DcDiV0yVM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V7IjxC6DcDiV0yVM .rough-node .label text,#mermaid-svg-V7IjxC6DcDiV0yVM .node .label text,#mermaid-svg-V7IjxC6DcDiV0yVM .image-shape .label,#mermaid-svg-V7IjxC6DcDiV0yVM .icon-shape .label{text-anchor:middle;}#mermaid-svg-V7IjxC6DcDiV0yVM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V7IjxC6DcDiV0yVM .rough-node .label,#mermaid-svg-V7IjxC6DcDiV0yVM .node .label,#mermaid-svg-V7IjxC6DcDiV0yVM .image-shape .label,#mermaid-svg-V7IjxC6DcDiV0yVM .icon-shape .label{text-align:center;}#mermaid-svg-V7IjxC6DcDiV0yVM .node.clickable{cursor:pointer;}#mermaid-svg-V7IjxC6DcDiV0yVM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V7IjxC6DcDiV0yVM .arrowheadPath{fill:#333333;}#mermaid-svg-V7IjxC6DcDiV0yVM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V7IjxC6DcDiV0yVM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V7IjxC6DcDiV0yVM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V7IjxC6DcDiV0yVM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V7IjxC6DcDiV0yVM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V7IjxC6DcDiV0yVM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V7IjxC6DcDiV0yVM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V7IjxC6DcDiV0yVM .cluster text{fill:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM .cluster span{color:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-V7IjxC6DcDiV0yVM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V7IjxC6DcDiV0yVM rect.text{fill:none;stroke-width:0;}#mermaid-svg-V7IjxC6DcDiV0yVM .icon-shape,#mermaid-svg-V7IjxC6DcDiV0yVM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V7IjxC6DcDiV0yVM .icon-shape p,#mermaid-svg-V7IjxC6DcDiV0yVM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V7IjxC6DcDiV0yVM .icon-shape .label rect,#mermaid-svg-V7IjxC6DcDiV0yVM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V7IjxC6DcDiV0yVM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V7IjxC6DcDiV0yVM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V7IjxC6DcDiV0yVM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} FGC 间隔很短(分钟级甚至秒级)
且 FGC 后老年代回收不掉多少
YGC 频繁但 FGC 正常
结局一: 内存泄漏
static Map 无上限缓存 /
ThreadLocal 没 remove /
监听器没注销
结局二: 业务对象本来就多
(数据量判断合理, 无泄漏)
8G 以上
16G+ 且对停顿敏感
⚠️ 生产注意
线程状态健康 + CPU 中等
但接口毛刺/超时
jstat -gc PID 1000 10
两次采样算差值
FGC 频率?
老年代被占满 → 回收赶不上
预埋 -XX:+HeapDumpOnOutOfMemoryError
或择时 jmap -dump:format=b,file=heap.hprof PID
年轻代太小 / 对象产生太快
→ 调大 -Xmn 或减少对象创建
MAT 打开 hprof
Dominator Tree 找最大 retained heap
Leak Suspects 报告
大对象是什么? 谁拽着它(GC Root)?
修代码 : 切断引用链
缓存加上限+过期 / finally 里 remove
调参数 :
① 调大堆 -Xmx
② 调年轻代比例 -Xmn / SurvivorRatio
③ 换收集器: 主推 G1
(-XX:MaxGCPauseMillis 定停顿目标)
堆多大?
G1 首选
ZGC: 停顿 <1ms
但小堆用它不划算
(不是'至少16G才能跑', 是大堆收益才明显)
手动 dump = STW + 文件≈堆大小
务必低峰期或摘流量后操作
jmap -histo:live 会先触发一次 FGC, 慎用
关键纠偏(FGC 与 CPU 的关系,分收集器谈):
- Parallel Old(JDK8 默认)FGC = 多线程 STW → 瞬间吃满所有核,但单次短
- CMS 并发失败退化成 Serial Old FGC = 单线程 → 只吃一个核,但能停几十秒
- ZGC = 几乎全程并发 → 不飙 CPU 也不怎么停
所以正确口径:单次 FGC 飙不飙 CPU 看收集器;频繁 FGC 无论用什么收集器都卡。
dump 手段阶梯:为什么不死等 OOM
先纠正两个直觉:
-XX:+HeapDumpOnOutOfMemoryError是 OOM 抛异常时 触发,不是 FGC 时------FGC 是常规动作,不会留下快照;- "对象激增,秒级下肯定出 FGC,等一下即可"------不成立:老年代还有余量时,可能 YGC 高频挣扎很久、对象慢慢晋升,FGC 几分钟甚至几小时后才来,等,就是被动的,现场可能迟到;
- 所以正确姿势是主动出击,按代价从小到大排阶梯:
#mermaid-svg-F56WUcFS9V0Pqd3f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F56WUcFS9V0Pqd3f .error-icon{fill:#552222;}#mermaid-svg-F56WUcFS9V0Pqd3f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F56WUcFS9V0Pqd3f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F56WUcFS9V0Pqd3f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F56WUcFS9V0Pqd3f .marker.cross{stroke:#333333;}#mermaid-svg-F56WUcFS9V0Pqd3f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F56WUcFS9V0Pqd3f p{margin:0;}#mermaid-svg-F56WUcFS9V0Pqd3f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f .cluster-label text{fill:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f .cluster-label span{color:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f .cluster-label span p{background-color:transparent;}#mermaid-svg-F56WUcFS9V0Pqd3f .label text,#mermaid-svg-F56WUcFS9V0Pqd3f span{fill:#333;color:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f .node rect,#mermaid-svg-F56WUcFS9V0Pqd3f .node circle,#mermaid-svg-F56WUcFS9V0Pqd3f .node ellipse,#mermaid-svg-F56WUcFS9V0Pqd3f .node polygon,#mermaid-svg-F56WUcFS9V0Pqd3f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F56WUcFS9V0Pqd3f .rough-node .label text,#mermaid-svg-F56WUcFS9V0Pqd3f .node .label text,#mermaid-svg-F56WUcFS9V0Pqd3f .image-shape .label,#mermaid-svg-F56WUcFS9V0Pqd3f .icon-shape .label{text-anchor:middle;}#mermaid-svg-F56WUcFS9V0Pqd3f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F56WUcFS9V0Pqd3f .rough-node .label,#mermaid-svg-F56WUcFS9V0Pqd3f .node .label,#mermaid-svg-F56WUcFS9V0Pqd3f .image-shape .label,#mermaid-svg-F56WUcFS9V0Pqd3f .icon-shape .label{text-align:center;}#mermaid-svg-F56WUcFS9V0Pqd3f .node.clickable{cursor:pointer;}#mermaid-svg-F56WUcFS9V0Pqd3f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F56WUcFS9V0Pqd3f .arrowheadPath{fill:#333333;}#mermaid-svg-F56WUcFS9V0Pqd3f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F56WUcFS9V0Pqd3f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F56WUcFS9V0Pqd3f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F56WUcFS9V0Pqd3f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F56WUcFS9V0Pqd3f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F56WUcFS9V0Pqd3f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F56WUcFS9V0Pqd3f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F56WUcFS9V0Pqd3f .cluster text{fill:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f .cluster span{color:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F56WUcFS9V0Pqd3f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F56WUcFS9V0Pqd3f rect.text{fill:none;stroke-width:0;}#mermaid-svg-F56WUcFS9V0Pqd3f .icon-shape,#mermaid-svg-F56WUcFS9V0Pqd3f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F56WUcFS9V0Pqd3f .icon-shape p,#mermaid-svg-F56WUcFS9V0Pqd3f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F56WUcFS9V0Pqd3f .icon-shape .label rect,#mermaid-svg-F56WUcFS9V0Pqd3f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F56WUcFS9V0Pqd3f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F56WUcFS9V0Pqd3f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F56WUcFS9V0Pqd3f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 嫌疑类坐实, 需要引用链实锤
想常态化抓'还没到 FGC 的激增期'
替代入口
永远预埋的保险丝
发现对象激增
(jstat 差值异常)
第一级: jmap -histo PID
不带 :live 不触发 FGC
看对象数量/占用排行
多数问题这步就有答案
第二级: 摘流量后 dump
从负载均衡摘下一台
jmap -dump:live,format=b,file=heap.hprof
STW 只影响已摘流的机器
→ MAT 离线分析
第三级: JFR 常开
JDK11+ 免费, 开销 <2%
持续记录对象分配
JMC 看分配热点
Arthas heapdump
本质 = jmap 换皮(一样 STW)
价值在 attach 后可联动
dashboard / watch 辅助判断
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=...
兜底, 不指望它抓激增期
一句话:激增期用 histo + JFR 轻量观测定位嫌疑类,需要引用链实锤时摘一台机器 dump,OOM 保险丝永远预埋但不当主力。
调参的锚点:存活集估算 → 换收集器的时机
堆调多大? :
- 堆的本质是"给必须同时活着的对象留地方"------所以锚点不是"系统会有多少对象",而是 live set(存活集):FGC 之后还活着的对象总量;
- 拍脑袋估不准 → 压测跑满流量,看 GC 日志里 FGC 后老年代的水位 = 真实存活集;
- 经验配比:总堆 ≈ 存活集的 3~4 倍,老年代 ≥ 存活集的 1.5~2 倍(余量留给晋升波动和浮动垃圾);年轻代按"对象产生速率 × 期望 YGC 间隔"估(每秒造 100M 垃圾、希望 5 秒一次 YGC → 年轻代至少 500M);
-Xms = -Xmx设成相等,避免运行期动态扩容的抖动;- 反向验证:调完 FGC 频率应降到小时级以上甚至不发生------不达标 = 估小了,或者有泄漏没修干净。
那么什么时候才换收集器? :
- 换收集器治的是"停顿模式",治不了"对象太多"------对象太多的病,换什么收集器都是换个姿势加班;
- 所以顺序必须是:先修代码(泄漏/对象激增)→ 再调参数(堆/比例/停顿目标)→ 都不够了才换收集器;
- 触发信号:
| 触发信号 | 换的方向 |
|---|---|
| 堆 >4~8G,ParallelOld 的 FGC 停顿秒级,业务忍不了 | G1(-XX:MaxGCPauseMillis 可控停顿,JDK9+ 默认) |
| 堆 16G+ 且停顿极度敏感(交易、实时推荐) | ZGC / Shenandoah(停顿 <1ms) |
| 离线批处理、吞吐优先、停顿无所谓 | 别换,Parallel 系就是为这种场景生的 |
| 升 JDK 大版本(8→17/21) | 顺势换,新版本的 G1 成熟度远超 JDK8 时代 |
五、速查表:一张表背完整条链
| 场景 | 第一现象 | 定位命令 | 判断依据 | 调整方向 |
|---|---|---|---|---|
| A1 死循环 | us% 高,业务线程 90%+ | top -Hp → %x → jstack | 连抓3次栈顶同一行 | 修代码;重启+限流兜底 |
| A2 GC 加班 | GC 线程吃 CPU | jstat -gc 算差值 | YGC 秒级 / YGCT 猛涨 | 减对象 → 调大年轻代 → 换 G1 |
| A3 内核态忙 | sy% 高 | vmstat 看 cs | 上下文切换飙高 | 砍线程数、IO 批量化 |
| B1 锁竞争 | CPU 低,大量 BLOCKED | jstack 比对 locked/waiting | 锁里有慢操作或锁太粗 | 缩粒度、读写锁、分片锁 |
| B2 下游慢 | CPU 低,RUNNABLE 卡 socketRead | jstack 看栈顶 | 栈在 DB 驱动/HttpClient | 超时+熔断+查慢SQL,别调 JVM |
| B3 池耗尽 | 大量 WAITING | jstack 看池子等待点 | getConnection/queue.take | 查泄漏 → 查慢SQL → 调大池 |
| B4 STW 卡顿 | 状态正常但毛刺 | jstat 差值 → dump → MAT | FGC 频繁且收不掉 | 泄漏修代码 / 合理调参换 G1 |
| B5 死锁 | 功能卡死 CPU 低 | jstack 拉到最后 | Found deadlock | 统一持锁顺序 / tryLock |
六、面试话术(90 秒版)
"线上卡顿我只有一条分水岭:CPU 高不高。CPU 高说明有人在干活------用 top -Hp 找到吃 CPU 的线程,转 16 进制去 jstack 里对 nid:栈顶在自家循环就是代码问题;吃 CPU 的是 GC 线程就用 jstat 算差值,对象产生速率超过回收速率,GC 只能满负荷追,这时减少对象创建、调大年轻代、换 G1。CPU 低说明有人在等------jstack 看状态:BLOCKED 等锁,比对 locked 和 waiting to lock 找持锁者;RUNNABLE 但卡在 socketRead 是等下游,这是最大的陷阱,网络等待不显示成阻塞,该查慢 SQL 和超时设置而不是调 JVM;WAITING 是等池子,先查连接泄漏再调大。还有一种是状态都健康但接口毛刺,那是 STW 型卡顿,jstat 看 FGC 频率,频繁了就 dump 下来用 MAT 看支配树------dump 不死等 OOM,先 jmap -histo 粗判(不触发 FGC),要实锤就摘一台机器导,平时 JFR 常开抓分配热点;泄漏就修代码切引用链,业务对象确实多才调参,堆按存活集(压测 FGC 后的老年代水位)的 3~4 倍给,8G 以上换 G1,16G 以上停顿敏感才考虑 ZGC。死锁最简单,jstack 拉到最后 JVM 自己报告。整套下来,每个判断都有命令佐证,每个调整都对应具体原因,不存在'重启试试'这种动作。"