性能瓶颈代码怎找?

访客 源码剖析 1

本文目录导读:

  1. 第一阶段:确定方向(宏观定位)
  2. 第二阶段:精准定位(代码级)
  3. 第三阶段:常见“隐形”瓶颈(反直觉案例)
  4. 一个标准排查流程示例

定位性能瓶颈是性能优化的关键,没有“万能钥匙”,但有一套系统化的方法和工具链

我们遵循 “先假设,后验证,再定位” 的原则,即不靠猜,用数据说话

以下是寻找性能瓶颈代码的完整方法论,按从宏观到微观的顺序:

第一阶段:确定方向(宏观定位)

在深入代码之前,先回答:病在哪?是CPU烧了,内存爆了,还是网络等IO?

看监控与指标(系统级)

  • CPU
    • :计算密集型(循环、加密、序列化、GC频繁)。
    • (但响应慢):可能是锁竞争、IO等待、网络延迟。
  • 内存:频繁Full GC、OOM、虚存使用率高 → 内存泄漏大对象(如超长字符串、缓存滥用)。
  • 磁盘IO:iowait高 → 数据库慢查询、日志写太多、文件读写。
  • 网络:带宽打满、连接数过多、丢包率高。

看应用层指标(APM工具)

  • 工具:SkyWalking、Pinpoint、Datadog、Arthas(阿里)。
  • 关键指标
    • 平均/TP99响应时间:哪个接口最慢?
    • 吞吐量:哪个接口下降最明显?
    • 错误率:异常导致重试,拖慢系统。

第二阶段:精准定位(代码级)

确定是某个接口或模块慢后,开始“解剖”。

针对 高CPU / 高内存

神器:Profiling(采样分析)

  • 工具Async Profiler(最推荐),IntelliJ ProfilerJProfilerYourKit
  • 怎么做
    • 采样:每秒抓取多次所有线程的调用栈。
    • 分析火焰图(Flame Graph)
      • 找出最宽的“平顶”,那通常是瓶颈函数。
      • Lock contention(锁竞争,红色区域)、GC(垃圾回收,紫色)、System calls(系统调用)。
    • 实战命令(Linux下)
      # 采集30秒CPU性能数据
      ./profiler.sh -d 30 -f flamegraph.svg <PID>

      SVG文件浏览器打开,点顶部的条块,立刻看到哪个方法最烧CPU。

针对 堆内存 / GC

工具: JMC(Java Mission Control)+ Flight RecorderVisualVMGCeasy(分析GC日志)

  • 怎么做
    • 开启GC日志:-Xlog:gc*:gc.log
    • GCeasy 分析日志:看GC暂停频率、原因(Allocation Failure, G1 Humongous Allocation)。
    • 内存转储(Heap Dump)
      • jmap -dump:live,format=b,file=heap.hprof <PID>(线上慎用,会停顿JVM)。
      • Eclipse MATVisualVM 分析:
        • 疑似泄漏 -> 看 Biggest Objects -> 找谁保持着这些对象的引用(Dominator Tree)。
        • 大对象 -> 比如一个 List 里有100万个元素,找出创建它的代码路径。

针对 慢查询 / 数据库

工具: MySQL慢查询日志EXPLAINASM(数据库监控)

  • 怎么做
    • 开启 slow_query_log,看哪些SQL超过1秒。
    • EXPLAIN 看是否全表扫描(type=ALL)、文件排序(Using filesort)、临时表。
    • N+1问题:看日志是否有成百上千条相似的SQL瞬间刷屏(常见于ORM框架,如Hibernate、MyBatis)。
    • 连接池耗尽:看 HikariPool 日志是否有 Connection is not available

针对 加锁 / 并发

工具: jstackAsync Profiler(锁分析)Thread Dump 分析工具(如 fastthread.io)

  • 怎么做
    • jstack -l <PID> 导出线程栈。
    • 搜索 BLOCKEDWAITINGPARKED
    • 线程状态
      • 大量线程处于 BLOCKED:锁竞争激烈,找出谁在持有锁(获得锁的线程)。
      • 大量线程处于 WAITING:可能是 Object.wait()LockSupport.park(),看谁在 notifyunpark
      • 死锁jstack 会自动打印 Found one Java-level deadlock,解决方式:重新设计锁顺序或使用 tryLock
    • 工具Async Profiler-e lock 参数可以直接采样锁竞争。

第三阶段:常见“隐形”瓶颈(反直觉案例)

  • 看似慢查询,其实是网络:数据库在本地,查询1ms,但接口耗时200ms,检查 网络IO线程模型序列化(JSON来回转几十次)。
  • 看似CPU高,其实是GCtop 看到CPU高,但火焰图里GC占比低?可能是内存分配过快导致频繁 Young GC,CPU花在扫描新生代,解决办法:减少对象创建(如使用池化、避免循环内new对象)。
  • 看似代码慢,其实是日志:在 for 循环里打印 logger.debug(),但日志级别是 INFO,不会影响啊?错!字符串拼接 "user: " + user + " time: " + time 这个表达式已经执行了,只是不输出,10万次循环,每次拼接3个对象,可能拖慢几百毫秒。使用占位符 "user: {}, time: {}", user, time
  • 看似代码,其实是操作系统:上下文切换过高(vmstat 1cs 列),高并发下线程数太多,操作系统忙于调度。

一个标准排查流程示例

假设一个Java服务响应变慢。

  1. top:CPU 80%,内存正常。
  2. pidstat -t -p <PID> 1:发现某个线程(如 http-nio-8080-exec-15)占用CPU特别高。
  3. printf "%x\n" 线程PID:把十进制的线程PID转成16进制。
  4. jstack <PID> | grep -A 20 16进制线程号:看到该线程正在执行 UserController.getOrderList() 方法。
  5. 打开Arthas
    • trace UserController getOrderList '#cost > 200':追踪该方法,发现 Database.query() 耗时1500ms,远超其他。
  6. 检查慢查询日志:发现SQL SELECT * FROM orders WHERE user_id=? 没有索引。
  7. 加索引:问题解决。

核心要点:

  • 不要凭感觉:用 traceprofilejstackheap dump 来证实。
  • 由外到内:系统 -> 进程 -> 线程 -> 代码行 -> 数据。
  • 关注“热”点:最频繁执行的代码、耗时最长的操作、占用内存最大的对象。

有了这些方法论和工具,你就可以像侦探一样,一步步找到性能瓶颈的“真凶”了。

标签: 代码优化

抱歉,评论功能暂时关闭!