分时流量步进法旨在识别老年代内存缓慢持续堆积,关键在设计观测窗口、排除干扰、定义有效斜率;需稳压15–20分钟、固定流量增幅、禁用元空间扩容与G1自适应调优;斜率计算剔除晋升抖动、碎片化假象和缓存预热,连续3阶段斜率>0.8MB/100TPS且老年代使用率单向爬升超1.5%/小时即判定慢性泄漏;配套须开启G1混合回收日志、每阶段执行native memory检查、比对ThreadLocalMap大小与线程数。

分时流量步进法不是为了“测出一个数字”,而是为了在真实业务节奏中,识别老年代内存是否在缓慢、持续、不可逆地堆积——这种堆积往往不触发 Full GC,却在大促中后期突然引爆 OOM。关键不在“怎么测”,而在“怎么设计观测窗口+怎么排除干扰+怎么定义斜率有效”。
一、为什么必须用分时流量步进,而不是一次性拉满
老年代慢性泄漏(如 ThreadLocal 持有业务上下文未清理、静态缓存无淘汰、监听器注册未注销)的特点是:单次请求内存增长极小(KB 级),但随时间累积、随线程复用放大。若直接打到峰值流量,GC 行为剧烈、日志噪声大、堆变化被 Young GC 和 Mixed GC 掩盖,根本无法分离“正常晋升”和“异常滞留”。分时步进的本质是制造可控的“时间放大镜”:
- 每阶段稳压 15–20 分钟,确保 JVM 进入稳态(GC 频率、各代使用率波动 ≤5%)
- 相邻阶段间流量增幅固定(如 +20% TPS),避免跳跃导致 GC 策略突变
- 全程禁用元空间动态扩容(-XX:MaxMetaspaceSize=2G)、关闭 G1 的自适应调优(-XX:-G1UseAdaptiveIHOP),消除非业务变量
二、如何精准提取老年代“净增长斜率”
不能只看老年代“当前使用量”,要剔除三类伪增长:
- 晋升抖动:Young GC 后进入老年代的对象,可能在下一波 Mixed GC 中又被回收 → 看 老年代存活对象数(而非容量),用 jstat -gc 输出的 OU(Old Used)结合 jmap -histo 统计长期存活类实例数交叉验证
- 碎片化假象:老年代碎片升高导致可用空间下降,但实际对象未增 → 对比 老年代已用容量(OU)与最大可分配块(max block size)趋势,若 OU 上升而 max block 下降明显,优先排查碎片
- 缓存预热效应:前 5 分钟因加载配置/初始化缓存导致的上升 → 每阶段取 第 10–20 分钟的 OU 均值作为该阶段基准点
斜率 = (阶段 N 老年代均值 OU – 阶段 N−1 老年代均值 OU) ÷ (阶段 N 流量 – 阶段 N−1 流量)。连续 3 个阶段斜率 > 0.8 MB/100 TPS,且伴随 Full GC 未发生但老年代使用率单向爬升超 1.5%/小时,即判定为慢性泄漏。
三、配套必须做的动作,否则斜率无意义
斜率本身只是信号,定位根因才靠这三件事:
- 开启 G1 的详细混合回收日志:-XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy -Xlog:gc+ergo*=trace,确认 Mixed GC 是否真在回收老年代对象(看 “Mixed GCs processed X regions” 后是否有 “evacuation failed” 或 “to-space exhausted”)
- 每阶段结束强制一次 jcmd VM.native_memory summary scale=MB,排除本地内存(Direct Buffer、CodeCache)误判为堆泄漏
- 对比线程池活跃线程数与 ThreadLocalMap 大小:用 jstack 抓线程快照后 grep “java.lang.ThreadLocal$ThreadLocalMap”,若 Map size 持续增长且与活跃线程数正相关,基本锁定 ThreadLocal 泄漏










