Skip to content

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/内存/网络/磁盘达成目标消耗多少资源
队列长度/等待时间系统是否在积累未完成工作
成本/每千请求商业资源效率

示例目标:

text
订单查询在稳态1500 QPS下:
成功率 >= 99.95%
P99 <= 200ms
单实例CPU <= 70%
容器RSS <= 1.5GiB
Young GC P99 <= 30ms
测试持续30分钟且无持续队列增长

只有平均延迟50ms不能证明P99满足200ms,也不能证明系统没有每分钟5秒停顿。

二、端到端延迟不只属于JVM

mermaid
flowchart TD
    A["客户端排队/网络"] --> B["Nginx/Gateway"]
    B --> C["Web线程池排队"]
    C --> D["Java业务代码"]
    D --> E["锁/线程池/连接池"]
    E --> F["Redis/DB/MQ/远程HTTP"]
    F --> G["序列化与网络返回"]

总响应时间近似:

text
入口等待
+ 应用队列等待
+ CPU执行
+ 锁等待
+ GC/安全点暂停
+ 数据库连接等待与SQL
+ 下游网络/服务等待
+ 响应序列化和发送

CPU低且接口慢时,继续调GC线程通常没有意义;更可能卡在连接池、锁、SocketRead、队列或限流等待。

三、Little定律建立容量直觉

稳态系统中:

text
L = λ × W
  • L:系统内平均在途请求/任务数。
  • λ:平均完成/到达速率。
  • W:平均停留时间。

例如1000 QPS、平均响应100ms:

text
L = 1000 × 0.1 = 100

平均约100个请求同时在系统中。若每个在途请求连带占用200KiB对象图,仅请求态平均就约20MiB,还未计算长尾、队列和框架开销。

Little定律不直接告诉你线程池应该正好100线程;异步I/O、CPU阶段、下游连接池和延迟分布都会影响设计。但它能检查容量说法是否自洽。

四、到达率超过服务能力,队列必然堆积

线程池16个Worker,单任务平均占用Worker 50ms,理想上限约:

text
16 / 0.05 = 320 tasks/s

若持续到达400 tasks/s:

text
净堆积速度 ≈ 400 - 320 = 80 tasks/s

一分钟约积压4800个任务。扩队列只会推迟拒绝,并增加等待时间和内存;扩线程若下游数据库仍只允许20个连接,会把等待从线程池队列转移到连接池。

mermaid
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余量。

六、先建立性能基线

调优前记录:

text
代码commit/镜像Digest
JDK完整版本与发行版
JVM完整参数与最终Flags
CPU核数、容器quota、内存limit
实例数和负载均衡策略
数据库/Redis/MQ版本和容量
流量模型、数据规模和缓存冷热
吞吐、延迟分位、错误率
CPU、RSS、Heap、GC、线程、队列、连接池

没有基线就无法证明优化,也无法判断退化来自参数、代码、流量或环境。

七、压测模型必须代表生产

7.1 封闭模型

固定虚拟用户:一个请求返回后才发下一个。系统变慢时,发压速率也自动下降,容易掩盖过载。

7.2 开放模型

按外部到达率持续发请求,更接近真实用户/消息流量。系统变慢时仍会继续到达,可观察排队、拒绝和过载恢复。

7.3 Coordinated Omission

若压测工具因为上一个请求卡住而停止按计划发送后续请求,最糟糕窗口没有被采样,P99会被低估。

text
计划每10ms发一个请求
某次服务暂停1秒
封闭工具只记录一个1秒慢请求
实际上这一秒本应到达约100个请求并排队

选择支持恒定到达率/遗漏修正的工具,并同时观察服务端真实排队和吞吐。

八、预热、稳态和重复实验

Java性能受类加载、JIT编译、缓存、连接池和数据库Buffer Pool预热影响。

一次合格实验通常包含:

mermaid
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:锁竞争与等待。

详见火焰图采样原理

十、第一棵诊断决策树

mermaid
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:

bash
top -p <pid>
top -H -p <pid>
pidstat -p <pid> -t 1

拿到十进制线程ID,例如12345,转换为十六进制:

bash
printf '%x\n' 12345

在jstack找:

text
nid=0x3039

连续抓3份以上:

bash
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、事务过长
BLOCKEDsynchronized竞争
LockSupport.parkJUC锁、队列、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。
  • 内存/线程栈。
  • 任务超时和重试。

约束。

mermaid
flowchart TD
    A["请求"] --> B["业务线程池32"]
    B --> C["数据库连接池16"]
    C --> D["数据库最大安全并发20"]
    B --> E["HTTP连接池8"]
    E --> F["下游限额10QPS"]

把业务线程从32扩到200,只会让更多线程等待16个DB连接和8个HTTP连接,并增加上下文切换与栈内存。

重点监控:

text
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火焰图。
  • 业务吞吐随线程数增加反而下降。

修复优先级:

  1. 缩短锁内代码和I/O。
  2. 不在锁内访问数据库/远程服务。
  3. 拆分全局锁为按key/分段锁,但治理key生命周期。
  4. 使用不可变快照、并发集合或消息串行化。
  5. 检查伪共享与缓存行竞争。

换成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趋势

text
Eden锯齿且Old稳定
→ 常见短命对象分配,关注频率与CPU

Young GC后Old持续增长,Full后回落
→ 工作集/晋升,需要看周期和容量

Full后Old基线仍持续上涨
→ 强引用持有、泄漏或真实Live Set增长

Heap稳定但RSS持续上涨
→ Direct、Metaspace、线程栈、Code Cache、Native

Allocation Profile回答“谁分配得快”,Heap Dump/Path To GC Roots回答“谁还活着、为什么”。二者不能互相替代。

十八、Heap大小怎样调

Heap至少满足:

text
真实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能力。

切换收集器属于架构级运行时变更,需要同一流量、数据和资源下比较:

text
P99/P999
吞吐
错误率
GC CPU
RSS
Live Set
最长暂停
故障恢复

详见HotSpot收集器完整原理

二十、JDK7/8 GC日志与JDK9+边界

JDK7/8:

bash
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/data/logs/gc.log

JDK9+:

bash
-Xlog:gc*,safepoint:file=/data/logs/gc.log:time,uptime,level,tags

迁移JDK时同时验证日志采集、轮转、告警解析和时区。不能把-Xlog复制到JDK8。

二十一、JIT预热与分层编译

Java方法可能经历:

mermaid
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,也可能是编译和去优化。

观察:

bash
jstat -compiler <pid> 1000 10
jcmd <pid> Compiler.codecache

详细逃逸分析见JIT逃逸分析

二十三、Code Cache与编译线程

JIT机器码存入Code Cache。Code Cache接近耗尽可能出现编译停止/性能退化警告。盲目增大还会增加本地内存预算。

编译线程过多可能与业务/GC争CPU,特别是在容器CPU quota较小、JVM却错误感知宿主机核数的旧JDK8环境。必须在目标容器中检查:

java
Runtime.getRuntime().availableProcessors()

并读取实际Flags和cgroup throttling。

二十四、安全点和非GC停顿

STW不只由GC触发,还可能与偏向锁撤销、反优化、类重定义、某些诊断操作等相关。安全点日志和JFR有助于区分:

  • 到达安全点耗时。
  • 安全点内部操作耗时。
  • 原因和频率。

JDK7/8历史参数与JDK9+统一日志不同。JDK9+可使用:

bash
-Xlog:safepoint

JDK8应使用目标版本支持的历史安全点统计参数并先在测试环境验证。

二十五、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余量。

证据:

bash
jcmd <pid> VM.flags
jcmd <pid> VM.native_memory summary

NMT必须启动时启用,并且它自身有开销;框架还应暴露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稳定。

证据

  1. top-H确认多个业务线程占CPU。
  2. 连续线程栈落在JSON序列化。
  3. CPU火焰图显示60%在反射字段访问和重复日期格式化。
  4. Allocation图显示同一DTO被转换三次。

修复

  • 避免Controller、日志、缓存分别重复序列化。
  • 预计算/复用安全格式化器。
  • 限制返回字段和分页。
  • 比较优化前后CPU、P99、响应大小和错误率。

不需要先切GC,因为证据指向业务CPU。

三十、商业场景二:CPU低但P99高

现象

CPU 30%,Heap稳定,P50 60ms但P99 5s。

证据

  • 线程栈大量等待HikariPool连接。
  • 连接池active=max、pending持续增长。
  • 数据库慢查询在报表SQL。
  • 事务持有连接期间调用远程HTTP。

修复

  1. 优化索引与SQL并限制报表范围。
  2. 把远程调用移出数据库事务。
  3. 设置连接获取/SQL/HTTP超时。
  4. 连接池容量与数据库安全连接数统一预算。
  5. 不只是把池从20改200。

三十一、商业场景三:批量任务造成GC风暴

现象

每小时导入文件时Young GC从每分钟2次升到每秒数次,并出现晋升洪峰。

原因链

mermaid
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压力增加。

应比较:

text
分区级Lag
生产速率
成功消费速率
重试/失败速率
单消息P95/P99
DB/HTTP池等待
消费者CPU/GC

扩容只提高某一层并发,不能突破共享瓶颈。

三十三、JDK7/8可运行容量Demo

java
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);
    }
}

预期:

text
capacityPerSecond=320.0
arrivalPerSecond=400.0
queueGrowthPerSecond=80.0
stable=false
littleLawConcurrency=40.0

这里Little定律的40使用给定平均延迟100ms,但系统若持续过载就没有真正稳态,延迟和在途数会继续增长;这正说明使用公式前必须检查稳态条件。

三十四、调优变更实验模板

text
问题:订单查询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%。

回滚:
  索引/代码版本可独立回退;观察锁与复制延迟。

每轮尽量只改变一个主要因素,否则无法归因。

三十五、生产发布与回滚

  1. 保存变更前JDK、Flags、指标和Profile。
  2. 在代表性环境复现。
  3. 固定镜像Digest和配置版本。
  4. 金丝雀少量实例。
  5. 观察完整业务周期,而非只看5分钟平均。
  6. 设置自动/人工硬回滚条件。
  7. 扩大流量时继续观察共享下游。
  8. 复盘是否真正消除根因。

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不稳定,也要同时看错误率和超时请求是否被排除。

三十八、关联知识点

三十九、学习验收

  • [ ] 能把“系统慢”改写成可量化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、错误率和资源同时证明改善,并能安全回滚,才算完成一次调优。