若GC日志中Full GC频繁(>1次/分钟)且单次耗时>500ms,可断定瓶颈在JVM内存模型或配置,如堆过小、对象晋升过快或元空间泄漏;反之,若YGCT与FGCT均低位但响应仍飙升,则问题在业务代码。

看 GC 日志里有没有“Stop-The-World”暴增
GC 是 JVM 层最直接的性能信号。如果 GC.log 里频繁出现 Full GC,且单次耗时 >500ms、频率 >1 次/分钟,基本可以断定瓶颈在 JVM 内存模型或配置上——比如堆太小、对象晋升过快、元空间泄漏。反之,如果 YGCT(年轻代 GC 时间)和 FGCT(Full GC 时间)都稳定在低位,但接口响应仍飙升,那问题大概率不在 GC,得往业务代码查。
- 用
jstat -gc <pid> 1000 5</pid>快速扫一眼:若OGC(老年代已用容量)持续逼近OGCMX(老年代最大容量),先怀疑内存泄漏或对象生命周期失控 - 加启动参数
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log,别用旧版-verbose:gc - 注意区分:G1 的
mixed GC不等于Full GC,但若 mixed 频次高且耗时长,也说明老年代压力大
用 jstack 看线程是不是集体“卡住”在业务方法里
如果大量线程状态是 RUNNABLE,但堆栈末尾停在你自己的类和方法(比如 com.example.service.OrderService.process()),而不是 Object.wait、LockSupport.park 或数据库驱动调用,那 CPU 正在被业务逻辑吃掉——可能是算法复杂度高、循环嵌套深、正则回溯爆炸,或者同步块没控制好粒度。
- 执行
jstack <pid> > threaddump.txt</pid>后,搜java.lang.Thread.State: RUNNABLE,再往下翻几行看实际执行点 - 特别警惕
at java.util.regex.Pattern$Loop.match(...)这类正则卡死,或at java.util.HashMap.get(...)在高并发下扩容锁表 - 如果
RUNNABLE线程集中在某个工具类(如 JSON 序列化、日期格式化),说明该组件成了热点,不是 JVM 问题,是选型或用法问题
对比 CPU 和 GC 时间占比是否严重失衡
用 top -H -p <pid></pid> 看 Java 进程内各线程的 CPU 占用,再结合 jstat -gc <pid></pid> 输出的 YGCT+FGCT 总和。如果进程总 CPU 使用率是 80%,但 GC 时间只占 2%~3%,剩下 77% 都在用户态跑代码,那 JVM 调优再猛也没用——得优化 for 循环、拆分大事务、减少远程调用次数。
- 一个粗略经验阈值:GC 时间占比 >15% 就值得优先排查 JVM;
- 注意:JDK 8u282+ 默认开启
-XX:+UseContainerSupport,在 Docker 里不设-XX:MaxRAMPercentage容易让 JVM 误判可用内存,导致 GC 频繁——这不是代码问题,是容器环境适配漏了 - Async-Profiler 可以生成火焰图:
./profiler.sh -e cpu -d 30 -f profile.html <pid></pid>,一眼看出 CPU 时间砸在哪一行业务代码上
检查是否所有慢请求都复现同一段调用链
如果 APM 工具(如 SkyWalking、Pinpoint)显示,95% 的慢请求都经过 PaymentService.calculateFee() → RateEngine.getRate() → CacheLoader.load() 这条路径,且该路径里没涉及锁、没 IO 等待、没 GC 日志关联,那就不用猜了——就是这段业务逻辑本身效率低。JVM 再稳,也救不了 O(n²) 的费率计算。
- 重点看方法耗时分布:如果
calculateFee()平均耗时 400ms,其中 390ms 都在getRate()里,而getRate()又 90% 时间花在一次本地缓存加载上,说明缓存失效策略或加载逻辑有缺陷 - 不要迷信“JVM 调优能解决一切”,很多所谓“性能问题”本质是业务建模错误,比如把实时风控规则全塞进单次 HTTP 请求里做同步校验
- 线上禁用
System.out.println和未配置 level 的日志框架,它们会隐式触发字符串拼接和锁竞争,造成假性 CPU 高
ConcurrentHashMap.computeIfAbsent 上。这种交叉影响,必须把 GC 时间戳和线程栈时间戳对齐看。


















