文档目录

2.4 预热、稳态与冷启动

上一节:2.3 微基准四大陷阱 | 下一节:2.5 内存与 GC 配套代码:02-jvm-performance-basics/04-warmup-steady-coldstart


一句话结论

同一份代码,在「刚启动」「跑了 30 秒」「跑了一小时」三个时刻,是三个不同的系统。 所以任何性能数字都必须标明它测的是哪一段——而判断「进入稳态」要靠数据(方差收敛),不是靠固定的秒数。


一、用运动员热身体理解三个阶段

你去跑 100 米:

阶段 状态 成绩
刚睡醒,直接跑 肌肉僵硬、心率没上来、动作不协调 15 秒
热身 10 分钟后跑 身体进入状态 12 秒
热身充分、连续跑几次 完全进入状态,成绩稳定 11.5 秒(波动很小)

关键洞察:

  1. 「热身多久」没有标准答案。 有人 5 分钟就够,有人要 20 分钟。唯一的判断标准是「成绩是否稳定了」,而不是「跑了多久」。
  2. 你不会把「刚睡醒那次 15 秒」算进你的成绩统计。 那是一次热身,不是测量。

JVM 完全一样,而且有三个阶段。


二、三个阶段分别是什么在变慢

阶段一:冷启动(进程刚起来)

正在发生 影响
JVM 启动、类加载 大量类需要从磁盘读取、验证、链接
所有代码在解释执行 最慢的执行形态
连接池是空的 需要建立数据库/Redis 连接
缓存是冷的 缓存穿透到数据库
线程池刚创建 线程创建开销
配置/密钥/证书加载 可能涉及网络

表现:第一个请求可能需要几百毫秒到几秒,而稳态是几十毫秒。

阶段二:预热期(跑了十几秒到几分钟)

正在发生 影响
热点方法升到 C1、C2 性能台阶式改善(第 1 节)
连接池填满 不用再等建连
JIT 的 profile 逐渐稳定 类型分布、分支走向被摸清
本地缓存逐渐升温 命中率上升

表现:数字不稳定,仍在下降。

阶段三:稳态

状态 说明
热点代码都在 C2 执行形态固定
缓存命中率稳定 冷热比例固定
GC 周期性运行 有规律的停顿,但分布稳定

表现:数字波动很小,这才是可以用于结论的数据。


三、怎么判断「进入稳态」(这是本节最实用的部分)

❌ 错误做法:固定预热 N 轮

「JMH 默认预热 5 轮,那就够了。」
「压测前先跑 60 秒预热。」

为什么错:不同程序需要的预热量差几十倍。一个启动就加载 500 个类的应用,和一个只有 20 个类的工具,能在同一时间进入稳态吗?

✅ 正确做法:看方差是否收敛

方法:把测量窗口切成小段(例如每 1000 次调用、或每 10 秒一个点),计算滚动变异系数(CV = 标准差 / 均值),当它稳定在一个小值以下时,就认为进入了稳态。

采样点   耗时(ns/op)   滚动CV
   1        1180         —
   2         612       0.45    ← 波动很大
   3         342       0.38
   4         258       0.26
   5         240       0.12
   6         233       0.06
   7         231       0.03
   8         230       0.02    ← CV < 5%,进入稳态 ✅
   9         230       0.02

判据建议:

CV 判断
> 10% 明显没稳,继续预热
5% – 10% 接近稳态,再等等
< 5% 可以开始测量
< 2% 环境很干净,数据可信度高

注意:这套判据同时也在告诉你测量环境的质量。如果 CV 永远降不到 5% 以下,说明环境本身有噪声(后台进程、CPU 降频、邻居干扰),此时「优化前后对比」也会不可信。

这也解释了第 0 章为什么要先测「噪声底线」:CV 就是噪声底线的量化形式。


四、冷启动要单独度量(不要混进稳态)

冷启动不是「污染」,它本身就是一个必须测的指标,只是不能和稳态混在一起。

冷启动的度量方式

冷启动时间 = 从进程启动到「能稳定服务」的时间

具体可以拆成几段:

阶段 度量方式
进程启动 → 端口监听 启动日志时间戳
端口监听 → 健康检查通过 探针首次成功的时间
健康检查通过 → 达到稳态性能 第一个「P99 达标」的时间窗口

注意第三段:很多服务「健康检查通过」很快,但性能达到稳态要几分钟。如果负载均衡在这个窗口把大量流量切过来,会拖垮新实例。

冷启动对哪些场景是硬指标

场景 为什么重要
滚动发布 新实例刚上线时性能差,要注意流量切换策略
自动扩容 扩容出来的实例需要「暖机」时间,突发流量时可能来不及
Serverless 冷启动就是用户等待时间的一部分
CLI / 桌面应用 启动时间是核心体验指标

缓解手段(了解即可,第 8 章会再提)

手段 原理
启动时主动预热(合成请求打热点路径) 提前把热点代码推到 C2
懒加载 / 异步初始化 让服务先能响应,重活后台做
AppCDS / CRaC(类数据共享 / 检查点恢复) 减少类加载与启动开销
预留实例(provisioned concurrency) Serverless 场景保持一定数量的热实例

五、四个常见错误

错误 后果
把预热数据算进统计 P99 虚高(被冷启动的慢样本污染)
固定预热时长而不看方差 可能预热不足(数据偏高)或浪费大量时间
稳态测量时长太短(< 3 分钟) 可能覆盖不到一个 GC 周期/缓存过期周期
用「健康检查通过」当作稳态起点 此时性能可能还差好几倍

六、本节小结

  1. 同一份代码在冷启动 / 预热期 / 稳态三个阶段是三个不同的系统。
  2. 判断稳态的正确方法是看方差收敛(滚动 CV < 5%),而不是固定的秒数。
  3. CV 同时也是测量环境质量的指标:CV 降不下来,说明环境有噪声,优化对比也不可信。
  4. 冷启动必须单独度量,不能混进稳态;它本身就是滚动发布、扩容、Serverless 的关键指标。
  5. 「健康检查通过」不等于「性能达稳态」——两者之间可能差几分钟。

七、自测

  1. 你的压测报告写「预热 60 秒,稳态 5 分钟」。请说出这种写法的一个优点和两个潜在问题。
  2. 一个服务的滚动发布流程是「新实例健康检查通过后立刻接入全部流量」。用本节的机制解释这个流程的风险,并给出改进方案。
  3. 某团队测出的性能数字每次波动都很大(CV = 15%),他们以为是「服务不稳定」。请给出另外三种可能的原因。
  1. 优点:明确区分了预热与稳态,且稳态时长(5 分钟)够长,能覆盖 GC/缓存周期。潜在问题:① 固定 60 秒预热不保证进入稳态——应该用滚动 CV 判定,因为不同应用需要的预热量可能差很多;② 没有说明预热的方式(是空跑还是打真实流量?打多少 QPS?),如果预热流量太低,热点代码根本没被推到 C2;③ 没有说明稳态期间的负载是否恒定,以及冷缓存的切换点。
  2. 风险:新实例在「健康检查通过」时,代码大部分还在解释执行/C1 阶段,连接池和缓存都是冷的,真实处理能力可能只有稳态的 1/3 甚至更低。如果立刻接入全部流量,这个实例会超时、排队,甚至拖垮整个集群(因为上游的重试会打到其他实例)。改进方案:① 预热后再接流量——用探针或就绪钩子发起一段合成流量,等滚动 CV 收敛后再标记 ready;② 渐进式流量切换(类似金丝雀)——先给小比例流量,观察延迟与错误率达标后再逐步增加;③ 提高健康检查的门槛,让它检查「连接池已填充 + 预热完成」而不只是「端口能响应」。
  3. 可能原因:① 环境噪声——后台任务(备份、日志轮转、系统更新)、CPU 降频(governor 设为节能)、云主机邻居抢占(steal 时间高);② 测量方法问题——样本量太小、单次运行没有重复、把预热期数据算进了统计;③ 被测系统本身有周期性——GC 周期、缓存过期、定时任务、连接池回收,这些会在测量窗口内制造规律性波动(这种情况下要看波动是否与 GC 日志/定时任务时间戳对齐)。④ 还有一种可能:负载生成器自己不稳定(压测机 CPU 打满、临时端口耗尽)。