JVM性能分析、容量模型与生产调优完整方法
JVM性能调优不是“把Xmx调大、线程数翻倍、切成G1”。真正的调优是一个可证伪的工程实验:先定义业务目标,建立端到端基线,用证据定位瓶颈,提出单一假设,进行代表性压测和金丝雀,再根据硬指标决定保留或回滚。
OOM分类、Heap Dump和MAT细节已经放在OOM生产级排查手册与JVM排查工具;本页专注性能:吞吐、长尾延迟、队列、CPU、GC、锁、I/O、JIT和容器资源怎样形成完整因果链。
学习目标
学完后应能:
- 区分吞吐、平均延迟、P95/P99/P999、错误率和资源效率。
- 从业务SLO反推JVM调优目标,而不是先选参数。
- 使用Little定律解释并发数、到达率和响应时间。
- 说明服务率低于到达率时队列为什么必然增长。
- 解释利用率接近100%时长尾为何非线性恶化。
- 区分开放模型、封闭模型和Coordinated Omission压测问题。
- 设计包含预热、稳定期、代表性数据和固定变量的实验。
- 从CPU高/低、GC高/低、线程状态和下游指标选择排查路径。
- 把Linux线程ID转换为jstack中的nid并用连续栈定位热点。
- 正确使用CPU、Wall、Allocation和Lock火焰图。
- 根据分配率、存活集、晋升、GC后基线和暂停组成调优Heap/GC。
- 识别线程池、连接池、数据库和MQ形成的端到端背压链。
- 解释JIT预热、内联、去优化、Code Cache和安全点抖动。
- 识别容器CPU throttling、OOMKilled和宿主机噪声。
- 用最小变更、金丝雀、硬回滚条件完成生产闭环。
一、性能目标必须先可量化
“系统有点慢”不是可执行目标。商业服务至少定义:
| 指标 | 回答的问题 |
|---|---|
| 吞吐TPS/QPS | 单位时间完成多少有效请求/任务 |
| P50 | 典型请求体验 |
| P95/P99/P999 | 长尾和少数用户最差体验 |
| 错误率 | 超时、拒绝、5xx、业务失败占比 |
| 可用性 | 服务在窗口内满足承诺的比例 |
| CPU/内存/网络/磁盘 | 达成目标消耗多少资源 |
| 队列长度/等待时间 | 系统是否在积累未完成工作 |
| 成本/每千请求 | 商业资源效率 |
示例目标:
订单查询在稳态1500 QPS下:
成功率 >= 99.95%
P99 <= 200ms
单实例CPU <= 70%
容器RSS <= 1.5GiB
Young GC P99 <= 30ms
测试持续30分钟且无持续队列增长只有平均延迟50ms不能证明P99满足200ms,也不能证明系统没有每分钟5秒停顿。
二、端到端延迟不只属于JVM
flowchart TD
A["客户端排队/网络"] --> B["Nginx/Gateway"]
B --> C["Web线程池排队"]
C --> D["Java业务代码"]
D --> E["锁/线程池/连接池"]
E --> F["Redis/DB/MQ/远程HTTP"]
F --> G["序列化与网络返回"]总响应时间近似:
入口等待
+ 应用队列等待
+ CPU执行
+ 锁等待
+ GC/安全点暂停
+ 数据库连接等待与SQL
+ 下游网络/服务等待
+ 响应序列化和发送CPU低且接口慢时,继续调GC线程通常没有意义;更可能卡在连接池、锁、SocketRead、队列或限流等待。
三、Little定律建立容量直觉
稳态系统中:
L = λ × W- L:系统内平均在途请求/任务数。
- λ:平均完成/到达速率。
- W:平均停留时间。
例如1000 QPS、平均响应100ms:
L = 1000 × 0.1 = 100平均约100个请求同时在系统中。若每个在途请求连带占用200KiB对象图,仅请求态平均就约20MiB,还未计算长尾、队列和框架开销。
Little定律不直接告诉你线程池应该正好100线程;异步I/O、CPU阶段、下游连接池和延迟分布都会影响设计。但它能检查容量说法是否自洽。
四、到达率超过服务能力,队列必然堆积
线程池16个Worker,单任务平均占用Worker 50ms,理想上限约:
16 / 0.05 = 320 tasks/s若持续到达400 tasks/s:
净堆积速度 ≈ 400 - 320 = 80 tasks/s一分钟约积压4800个任务。扩队列只会推迟拒绝,并增加等待时间和内存;扩线程若下游数据库仍只允许20个连接,会把等待从线程池队列转移到连接池。
flowchart TD
A["到达率400/s"] --> B["线程池能力320/s"]
B --> C["净堆积80/s"]
C --> D["队列等待与内存上涨"]
D --> E["超时后客户端重试"]
E --> F["实际到达率进一步升高"]
F --> C五、利用率为什么不能长期接近100%
请求耗时有波动,GC、OS调度、网络和下游也有抖动。利用率越接近资源上限,系统吸收突发的余量越小,排队时间通常非线性增加。
所以:
- 平均CPU 95%并不代表“还剩5%可用”,P99可能已恶化。
- 数据库连接池始终100%说明请求在等待,不表示连接利用得很完美。
- 线程池active始终等于maximum且队列增长说明饱和。
- 容器CPU quota下宿主机CPU不高也可能发生CFS throttling。
容量规划需要保留突发、故障转移和GC余量。
六、先建立性能基线
调优前记录:
代码commit/镜像Digest
JDK完整版本与发行版
JVM完整参数与最终Flags
CPU核数、容器quota、内存limit
实例数和负载均衡策略
数据库/Redis/MQ版本和容量
流量模型、数据规模和缓存冷热
吞吐、延迟分位、错误率
CPU、RSS、Heap、GC、线程、队列、连接池没有基线就无法证明优化,也无法判断退化来自参数、代码、流量或环境。
七、压测模型必须代表生产
7.1 封闭模型
固定虚拟用户:一个请求返回后才发下一个。系统变慢时,发压速率也自动下降,容易掩盖过载。
7.2 开放模型
按外部到达率持续发请求,更接近真实用户/消息流量。系统变慢时仍会继续到达,可观察排队、拒绝和过载恢复。
7.3 Coordinated Omission
若压测工具因为上一个请求卡住而停止按计划发送后续请求,最糟糕窗口没有被采样,P99会被低估。
计划每10ms发一个请求
某次服务暂停1秒
封闭工具只记录一个1秒慢请求
实际上这一秒本应到达约100个请求并排队选择支持恒定到达率/遗漏修正的工具,并同时观察服务端真实排队和吞吐。
八、预热、稳态和重复实验
Java性能受类加载、JIT编译、缓存、连接池和数据库Buffer Pool预热影响。
一次合格实验通常包含:
flowchart TD
A["环境清理与版本固定"] --> B["Warm-up预热"]
B --> C["Ramp-up逐步加压"]
C --> D["Steady稳态采样"]
D --> E["Stress/峰值与恢复"]
E --> F["Cool-down观察积压消退"]
F --> G["多轮重复与对比"]必须固定/记录:
- 请求数据分布和命中率。
- 数据库数据量和索引。
- 实例规格与邻居负载。
- JDK、参数和代码。
- 测试持续时间。
只跑30秒通常只测到启动、预热或缓存冷态,不能代表长期GC和内存基线。
九、性能证据四件套
9.1 Metrics
适合回答趋势和相关性:
- 请求率、错误率、延迟分位。
- CPU、throttling、RSS、Page Fault。
- Heap/Old/Metaspace/Direct。
- GC次数、暂停和分配率。
- 线程池、队列、连接池。
9.2 Logs
适合异常上下文和离散事件:发布、超时、拒绝、GC日志、Fatal Error。日志本身也可能造成I/O和锁开销,不能在高频路径无限打印大对象。
9.3 Traces
适合把入口、应用、数据库和下游耗时连接起来。采样率和Header传播要准确;Trace中的跨度缺失不等于那段没有耗时。
9.4 Profiles
回答CPU花在哪里、谁在分配、谁持锁、谁等待。事件选错会得出错误结论:
- CPU Profile:实际占用CPU的热点。
- Wall Profile:运行和等待,适合CPU不高但接口慢。
- Allocation Profile:对象分配路径,不直接证明泄漏。
- Lock Profile:锁竞争与等待。
详见火焰图采样原理。
十、第一棵诊断决策树
flowchart TD
A["延迟/吞吐异常"] --> B{"CPU是否高"}
B -- "高" --> C{"GC CPU/暂停是否高"}
C -- "高" --> D["分配率、存活率、Heap和收集器"]
C -- "低" --> E["CPU Profile、热点循环、序列化、加密"]
B -- "低" --> F{"线程是否大量等待"}
F -- "连接池/Socket" --> G["下游、超时、池容量"]
F -- "BLOCKED/锁" --> H["Lock Profile与连续线程栈"]
F -- "队列" --> I["到达率与服务率、背压"]
F -- "无明显等待" --> J["CPU throttling、安全点、网络和观测缺口"]十一、CPU高的完整排查
Linux:
top -p <pid>
top -H -p <pid>
pidstat -p <pid> -t 1拿到十进制线程ID,例如12345,转换为十六进制:
printf '%x\n' 12345在jstack找:
nid=0x3039连续抓3份以上:
jstack -l <pid> > thread-1.txt
sleep 5
jstack -l <pid> > thread-2.txt
sleep 5
jstack -l <pid> > thread-3.txt同一线程连续停在同一计算栈更有证据。单份RUNNABLE不等于它一直高CPU,SocketRead也可能显示RUNNABLE但实际等待本地I/O语义。
11.1 常见CPU热点
- JSON序列化/反序列化和重复对象复制。
- 正则灾难性回溯。
- 加密、压缩、图片/文档处理。
- 无终止循环或重试风暴。
- 大量日志格式化。
- Hash冲突或错误集合算法。
- GC线程因高分配/存活率占CPU。
- 线程过多造成上下文切换。
最终应使用CPU火焰图/JFR验证热点占比,不只根据方法名猜。
十二、CPU不高但接口慢
这是线上最常见误判。线程通常在等待:
| 栈/指标 | 方向 |
|---|---|
| SocketRead/HTTP client | 下游慢、超时过长、网络 |
| 等数据库连接 | 连接池耗尽、慢SQL、事务过长 |
BLOCKED | synchronized竞争 |
LockSupport.park | JUC锁、队列、Future、线程池等待 |
Future.get/join | 同步等待、线程饥饿死锁 |
| 队列增长 | 到达率超过服务率 |
| CPU throttled | 容器被限速,宿主机CPU可能不高 |
需要同时看Wall Profile、Trace、连接池和下游指标。
十三、线程状态不能机械判断
- NEW:尚未启动。
- RUNNABLE:JVM可运行状态,可能正在CPU执行,也可能在某些本地I/O。
- BLOCKED:等待进入
synchronized监视器。 - WAITING:无期限等待通知/条件/park。
- TIMED_WAITING:带超时等待、sleep、parkNanos等。
- TERMINATED:执行结束。
大量WAITING可能是正常空闲线程池;大量BLOCKED在同一锁上更可疑。必须结合栈顶、锁拥有者、线程数量趋势和业务延迟。
十四、线程池调优必须连着下游
CPU密集任务的线程数常从可用CPU附近评估;I/O任务可能需要更多并发,但最终受:
- 数据库连接池。
- HTTP连接池。
- 下游QPS限制。
- 容器CPU。
- 内存/线程栈。
- 任务超时和重试。
约束。
flowchart TD
A["请求"] --> B["业务线程池32"]
B --> C["数据库连接池16"]
C --> D["数据库最大安全并发20"]
B --> E["HTTP连接池8"]
E --> F["下游限额10QPS"]把业务线程从32扩到200,只会让更多线程等待16个DB连接和8个HTTP连接,并增加上下文切换与栈内存。
重点监控:
submit rate
complete rate
active count
queue length
queue wait time
task execution time
reject count
timeout/retry count十五、锁竞争调优
证据:
- 连续线程栈大量线程BLOCKED在同一monitor。
- JFR Monitor Enter/Lock事件。
- Lock火焰图。
- 业务吞吐随线程数增加反而下降。
修复优先级:
- 缩短锁内代码和I/O。
- 不在锁内访问数据库/远程服务。
- 拆分全局锁为按key/分段锁,但治理key生命周期。
- 使用不可变快照、并发集合或消息串行化。
- 检查伪共享与缓存行竞争。
换成ReentrantLock不会自动消除竞争;它只是提供可中断、超时、公平性和Condition等能力。
十六、GC性能先看五个量
16.1 分配率
单位时间创建多少对象字节。分配率高会增加Young GC频率,但短命对象都能回收不等于泄漏。
16.2 存活集Live Set
GC后仍存活的对象总量。Heap必须容纳真实Live Set并有分配/复制余量。
16.3 对象存活率
Young GC前对象中有多少需要复制到Survivor/Old。高存活率会增加Evacuation和晋升成本。
16.4 晋升率
单位时间从Young进入Old多少数据。突发批量、Survivor不足或长任务可造成晋升洪峰。
16.5 暂停组成
Root扫描、Card/RSet、对象复制、引用处理、类卸载、安全点等待分别可能占时,不能只看总Pause。
十七、怎样读Heap趋势
Eden锯齿且Old稳定
→ 常见短命对象分配,关注频率与CPU
Young GC后Old持续增长,Full后回落
→ 工作集/晋升,需要看周期和容量
Full后Old基线仍持续上涨
→ 强引用持有、泄漏或真实Live Set增长
Heap稳定但RSS持续上涨
→ Direct、Metaspace、线程栈、Code Cache、NativeAllocation Profile回答“谁分配得快”,Heap Dump/Path To GC Roots回答“谁还活着、为什么”。二者不能互相替代。
十八、Heap大小怎样调
Heap至少满足:
真实Live Set
+ 正常分配波动
+ Young/Old复制或并发回收余量
+ 峰值业务对象
+ 安全余量Heap过小:
- GC过于频繁。
- 晋升/回收赶不上分配。
- Concurrent Mode Failure/To-space问题。
Heap过大:
- 占用更多容器/宿主机内存。
- Dump更大。
- 某些收集器Full/整理最坏暂停更长。
- 问题可能更晚暴露。
Xms=Xmx可减少扩堆变化,但增加初始承诺和高密度部署压力,不是所有服务绝对必须。
十九、选择GC而不是迷信GC
- Serial:小堆、少CPU、简单工具。
- Parallel:批处理/吞吐优先。
- CMS:JDK7/8历史低停顿方案,有碎片、浮动垃圾和并发失败;JDK9废弃、14移除。
- G1:Region、Young/Mixed、并发标记,JDK8可用、JDK9起常见默认;仍有STW、RSet和Humongous成本。
- ZGC/Shenandoah:现代低停顿选择,版本、发行版和吞吐成本必须压测,不属于JDK7/8能力。
切换收集器属于架构级运行时变更,需要同一流量、数据和资源下比较:
P99/P999
吞吐
错误率
GC CPU
RSS
Live Set
最长暂停
故障恢复二十、JDK7/8 GC日志与JDK9+边界
JDK7/8:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/data/logs/gc.logJDK9+:
-Xlog:gc*,safepoint:file=/data/logs/gc.log:time,uptime,level,tags迁移JDK时同时验证日志采集、轮转、告警解析和时区。不能把-Xlog复制到JDK8。
二十一、JIT预热与分层编译
Java方法可能经历:
flowchart TD
A["解释执行/采集Profile"] --> B["热点计数达到阈值"]
B --> C["C1快速编译/继续采集"]
C --> D["C2高层优化"]
D --> E["类型假设、内联、逃逸分析"]
E --> F{"运行时假设失效?"}
F -- "是" --> G["Deoptimization回退/重编译"]
F -- "否" --> H["持续执行优化机器码"]JDK7与JDK8的分层编译默认和成熟度有差异,必须以目标更新版Flags为准。
压测不预热会把类加载、解释执行和编译时间混入稳态。反过来,只测完全热态也可能忽略发布后冷启动SLO;应分别测启动和稳态。
二十二、内联、逃逸分析和去优化
JIT可:
- 内联小而热的方法,暴露跨方法优化机会。
- 标量替换不逃逸对象。
- 消除线程私有对象锁。
- 消除冗余边界/空检查。
- 基于运行时类型分布做推测优化。
若出现新实现类、类被重新定义或假设失效,JVM可能去优化,回到解释/低层代码并重编。发布后阶段性延迟抖动不一定是GC,也可能是编译和去优化。
观察:
jstat -compiler <pid> 1000 10
jcmd <pid> Compiler.codecache详细逃逸分析见JIT逃逸分析。
二十三、Code Cache与编译线程
JIT机器码存入Code Cache。Code Cache接近耗尽可能出现编译停止/性能退化警告。盲目增大还会增加本地内存预算。
编译线程过多可能与业务/GC争CPU,特别是在容器CPU quota较小、JVM却错误感知宿主机核数的旧JDK8环境。必须在目标容器中检查:
Runtime.getRuntime().availableProcessors()并读取实际Flags和cgroup throttling。
二十四、安全点和非GC停顿
STW不只由GC触发,还可能与偏向锁撤销、反优化、类重定义、某些诊断操作等相关。安全点日志和JFR有助于区分:
- 到达安全点耗时。
- 安全点内部操作耗时。
- 原因和频率。
JDK7/8历史参数与JDK9+统一日志不同。JDK9+可使用:
-Xlog:safepointJDK8应使用目标版本支持的历史安全点统计参数并先在测试环境验证。
二十五、JFR版本边界
JFR在Oracle JDK7u4/JDK8历史上与商业特性开关/许可背景相关,不同OpenJDK发行版支持不同;不能给所有JDK8统一命令。JDK11后JFR进入OpenJDK主线并可用jcmd JFR.start等方式。
生产使用前确认:
- JDK发行版和许可。
- 支持的事件与命令。
- 采样配置和磁盘保留。
- 敏感数据。
- 持续记录开销。
JDK8不具备目标能力时可使用async-profiler、JMC兼容版本、GC日志和线程栈组合,但要评估权限和Native Agent风险。
二十六、Direct Memory调优要纠正的误区
DirectByteBuffer的本地内存通常由与Java对象关联的清理机制最终释放;它不等于“只能等Old满后Full GC顺便回收”。直接内存压力还可能触发显式回收尝试和重试,具体实现随JDK变化。
真正风险:
- Cleaner依赖对象不可达与GC时机,不具备业务实时性。
- Netty ByteBuf有独立引用计数,漏
release()不会靠普通语义及时解决。 - Direct不计入Heap但计入RSS/cgroup。
- Xmx调大可能挤压Direct和Native余量。
证据:
jcmd <pid> VM.flags
jcmd <pid> VM.native_memory summaryNMT必须启动时启用,并且它自身有开销;框架还应暴露Direct/Allocator指标。
二十七、容器CPU Throttling
容器限制2核,进程短窗口想使用4核时,CFS quota可能把线程暂停到下一个周期。现象:
- 宿主机总CPU不高。
- 容器CPU接近limit。
- throttled periods/time增加。
- 接口P99抖动。
- GC/JIT/业务线程共同争2核。
不能通过继续增加线程解决。应:
- 检查requests/limits和实际核感知。
- 减少热点CPU和线程过量。
- 调整合理CPU配额。
- 使用目标JDK8更新版的容器感知能力。
- 压测验证故障转移时余量。
二十八、操作系统和宿主机噪声
JVM指标正常但延迟异常还要看:
- CPU steal、iowait和上下文切换。
- Swap和内存回收。
- 磁盘延迟/IOPS。
- 网络丢包、重传、DNS。
- NUMA远端内存访问(大机型)。
- 同宿主机其他容器竞争。
- 时间同步和时钟跳变。
“Java应用慢”不证明根因在JVM。
二十九、商业场景一:CPU高且GC正常
现象
订单列表接口CPU 95%,P99升到2秒,GC暂停低、Old稳定。
证据
- top-H确认多个业务线程占CPU。
- 连续线程栈落在JSON序列化。
- CPU火焰图显示60%在反射字段访问和重复日期格式化。
- Allocation图显示同一DTO被转换三次。
修复
- 避免Controller、日志、缓存分别重复序列化。
- 预计算/复用安全格式化器。
- 限制返回字段和分页。
- 比较优化前后CPU、P99、响应大小和错误率。
不需要先切GC,因为证据指向业务CPU。
三十、商业场景二:CPU低但P99高
现象
CPU 30%,Heap稳定,P50 60ms但P99 5s。
证据
- 线程栈大量等待HikariPool连接。
- 连接池active=max、pending持续增长。
- 数据库慢查询在报表SQL。
- 事务持有连接期间调用远程HTTP。
修复
- 优化索引与SQL并限制报表范围。
- 把远程调用移出数据库事务。
- 设置连接获取/SQL/HTTP超时。
- 连接池容量与数据库安全连接数统一预算。
- 不只是把池从20改200。
三十一、商业场景三:批量任务造成GC风暴
现象
每小时导入文件时Young GC从每分钟2次升到每秒数次,并出现晋升洪峰。
原因链
flowchart TD
A["一次读入整个文件"] --> B["创建大量行DTO/String/Map"]
B --> C["处理时间跨越多次Young GC"]
C --> D["临时对象晋升Old"]
D --> E["Old增长与Mixed/Full压力"]修复
- 流式读取和固定批大小。
- 每批写库后释放引用。
- 避免Map中间模型和重复String复制。
- 限制并行批次数。
- 根据真实Live Set再评估Young/Heap,而不是先把Xmx翻倍。
三十二、商业场景四:扩消费者短暂有效又堆积
MQ Lag扩容消费者后短暂下降,随后再次上涨:
- 新实例冷启动/JIT后吞吐变化。
- 数据库连接/锁成为共享瓶颈。
- 分区数限制有效并发。
- 热分区导致Lag分布不均。
- 下游限流、超时和重试放大。
- 单消息处理内存和GC压力增加。
应比较:
分区级Lag
生产速率
成功消费速率
重试/失败速率
单消息P95/P99
DB/HTTP池等待
消费者CPU/GC扩容只提高某一层并发,不能突破共享瓶颈。
三十三、JDK7/8可运行容量Demo
public class CapacityModelDemo {
public static void main(String[] args) {
int workers = 16;
double serviceMillis = 50.0;
double arrivalPerSecond = 400.0;
double capacityPerSecond =
workers * 1000.0 / serviceMillis;
double growthPerSecond =
arrivalPerSecond - capacityPerSecond;
System.out.println("capacityPerSecond="
+ capacityPerSecond);
System.out.println("arrivalPerSecond="
+ arrivalPerSecond);
System.out.println("queueGrowthPerSecond="
+ Math.max(0.0, growthPerSecond));
System.out.println("stable="
+ (arrivalPerSecond < capacityPerSecond));
double averageLatencySeconds = 0.1;
double concurrency = arrivalPerSecond
* averageLatencySeconds;
System.out.println("littleLawConcurrency="
+ concurrency);
}
}预期:
capacityPerSecond=320.0
arrivalPerSecond=400.0
queueGrowthPerSecond=80.0
stable=false
littleLawConcurrency=40.0这里Little定律的40使用给定平均延迟100ms,但系统若持续过载就没有真正稳态,延迟和在途数会继续增长;这正说明使用公式前必须检查稳态条件。
三十四、调优变更实验模板
问题:订单查询P99为600ms,目标200ms。
证据:
45% Wall时间等待DB连接;
DB连接active=20、pending峰值150;
慢SQLP99=450ms;
GC P99=12ms,CPU 45%。
假设:
根因是慢SQL和长事务占用连接,不是GC。
变更:
新增覆盖索引;远程调用移出事务;不改JVM参数。
验证:
相同数据和1000QPS压测30分钟;
对比P50/P99、错误率、DB CPU、连接等待。
硬门槛:
P99 <= 200ms;错误率不升;DB CPU <= 70%。
回滚:
索引/代码版本可独立回退;观察锁与复制延迟。每轮尽量只改变一个主要因素,否则无法归因。
三十五、生产发布与回滚
- 保存变更前JDK、Flags、指标和Profile。
- 在代表性环境复现。
- 固定镜像Digest和配置版本。
- 金丝雀少量实例。
- 观察完整业务周期,而非只看5分钟平均。
- 设置自动/人工硬回滚条件。
- 扩大流量时继续观察共享下游。
- 复盘是否真正消除根因。
GC/Heap参数变化可能改变启动、内存提交和故障形态,必须与容器limit、探针和滚动策略一起验证。
三十六、常见误区
| 误区 | 正确理解 |
|---|---|
| 平均延迟低就性能好 | P99/P999和错误率可能很差 |
| CPU越接近100%利用越充分 | 排队和长尾会非线性恶化 |
| 线程越多吞吐越高 | 下游、CPU、上下文切换和栈内存会限制 |
| 队列越大越不丢任务 | 只是延后拒绝并增加等待/内存 |
| CPU低说明应用没瓶颈 | 可能等待锁、连接、I/O或被throttle |
| Young GC频繁就是泄漏 | 可能只是短命对象分配率高 |
| Allocation热点就是泄漏 | 泄漏要看存活和GC Roots链 |
| Heap调大总能减少延迟 | 可能增加RSS、Dump和最坏停顿 |
| G1一定优于Parallel/CMS | 目标不同,必须同条件压测 |
| Direct Memory只能等Old Full GC回收 | 有引用清理与压力路径,但仍不可依赖及时性 |
| 64位JDK普遍比32位慢 | 这是过时泛化;现代生产应按支持版本和实测 |
| 多个32位JVM是利用大机器的默认方案 | 现代容器/64位JDK更常见,拆实例应基于故障域与容量 |
| 压测TPS达到目标就完成 | 还要持续时间、P99、错误、资源和恢复 |
三十七、面试标准回答
JVM性能调优完整流程
先定义业务SLO和基线,再用Metrics、Logs、Traces、Profiles定位端到端瓶颈;区分CPU、GC、锁、队列、连接池、I/O、JIT和容器throttling。提出单一可证伪假设,固定版本/数据/资源做预热和稳态压测,同时比较P99、吞吐、错误率、CPU、RSS和下游。最后金丝雀、设置硬回滚条件并复盘,参数是最后手段而不是起点。
CPU低但接口慢怎么查
先看连续线程栈和Wall Profile,定位SocketRead、数据库连接池、BLOCKED锁、Future等待或线程池队列;再用Trace和下游指标确认具体阶段。还要检查容器CPU throttling,因为宿主机CPU低不代表容器未被限速。CPU低时盲目换GC通常无效。
Young GC频繁怎样判断是否有问题
同时看分配率、每次暂停、业务P99、GC CPU、对象存活率和Old趋势。如果暂停短、CPU可接受、Old稳定,可能只是高短命分配;若存活/晋升高、Old基线上升或P99恶化,再查批量对象、Survivor、Heap和收集器。频率本身不是结论。
线程池如何估算
先按CPU/I/O阶段和服务时间估算,再受数据库连接池、HTTP连接池、下游限额和容器CPU约束。用提交率、完成率、active、队列等待、执行时间、拒绝和超时验证。若到达率长期大于完成率,队列必涨;加线程不能突破共享下游瓶颈。
为什么P99比平均值重要
平均值会把少量秒级停顿摊薄,但这些请求是真实用户超时。P99揭示排队、GC、锁、下游抖动和资源饱和的长尾。仍需结合请求量和窗口,因为样本很少时P999不稳定,也要同时看错误率和超时请求是否被排除。
三十八、关联知识点
- JVM参数来源与诊断命令
- GC可达性与回收算法
- HotSpot收集器与JDK7/8选型
- 对象分配、晋升与GC
- JIT逃逸分析
- 火焰图与采样
- JVM常用排查工具
- OOM生产级排查
- 线程池生产排查
- JavaSE面试标准回答
三十九、学习验收
- [ ] 能把“系统慢”改写成可量化SLO。
- [ ] 能用Little定律检查到达率、延迟和在途数。
- [ ] 能解释到达率超过服务率时队列为何必涨。
- [ ] 能设计避免Coordinated Omission的压测。
- [ ] 能根据CPU高/低选择CPU或Wall Profile。
- [ ] 能把Linux TID转换为jstack nid并连续取样。
- [ ] 能用分配率、Live Set、晋升、Old基线分析GC。
- [ ] 能解释线程池为什么受下游连接池限制。
- [ ] 能说明JIT预热、去优化和Code Cache抖动。
- [ ] 能识别容器CPU throttling与宿主机CPU差异。
- [ ] 能写出有假设、证据、硬门槛和回滚的调优实验。
本章小结
性能调优的核心不是参数数量,而是容量模型和证据链。Little定律与队列解释系统为何过载,Metrics/Trace/Profile解释时间花在哪里,GC/JIT/线程池和容器指标解释JVM如何影响业务。只有在代表性负载下用P99、错误率和资源同时证明改善,并能安全回滚,才算完成一次调优。
