Linux 性能调优实战:CPU 飙升与内存泄漏排查全指南
排查性能问题最核心的经验就一句话:先用宏观工具缩小范围,再用微观工具定位根因。 别一上来就 strace、gdb 满天飞,先搞清楚"问题出在哪个维度"——CPU、内存、IO 还是网络?
这篇我用两个真实案例,带你走一遍完整的排查流程。
一、CPU 飙升:从 load 飙高到定位精确代码行
1.1 发现异常
某天监控告警:线上 Web 服务器 CPU 使用率持续 95%+,load average 冲到 32。
第一反应不是看代码,而是确认现象:
$ top
top - 14:23:15 up 12 days, 3:21, 2 users, load average: 32.51, 28.30, 19.10
Tasks: 215 total, 1 running, 214 sleeping, 0 stopped, 0 zombie
%Cpu(s): 90.3 us, 5.1 sy, 0.0 ni, 4.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
us 占 90%,用户态吃掉了几乎所有 CPU。内核态 sy 只有 5%,说明不是系统调用的问题。
1.2 找到肇事进程
top 默认按 CPU 排序,直接看最上面:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 work 20 0 4658948 356984 12748 S 798.3 2.3 112:34.56 java
一个 Java 进程吃掉了接近 800% 的 CPU(32 核机器),内存倒还正常。确认是应用层的问题,可以上更细的工具了。
1.3 用 perf 采样热点函数
top 告诉我是哪个进程,但不知道它在干什么。这时候 perf 出场:
# 采样 30 秒
$ perf top -p 12345
排名第一的热点函数叫 org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream.write,占比 67%。
直觉告诉我:有人在循环里反复压缩数据,而且频率异常高。
1.4 定位到具体代码行
有了函数名,但不知道是哪行代码调的。用 perf record 抓调用栈:
$ perf record -g -p 12345 --sleep 30
$ perf report -g graph --no-children
调用链清晰显示:
GzipCompressorOutputStream.write (67%)
-> CompressionUtils.compress (62%)
-> ReportService.export (58%)
-> ReportController.exportData (57%)
看到 ReportController.exportData 我就有数了——每 5 秒轮询生成报表的逻辑。
回头看代码:
// 问题代码:每次轮询都全量压缩
public void exportData() {
String data = queryAllData(); // 查询全部数据
byte[] compressed = CompressionUtils.compress(data.getBytes()); // 压缩
saveToFile(compressed);
}
问题很清楚:queryAllData() 每次返回全量数据(几十 MB),然后全量压缩。轮询间隔 5 秒,等于每 5 秒压缩一次几十 MB。
1.5 解决方案
改动很小,效果显著:
// 修改后:增量导出,仅压缩增量部分
public void exportIncrementalData(Date lastExportTime) {
String data = queryIncrementalData(lastExportTime); // 只查增量
if (data.length() > 0) {
byte[] compressed = CompressionUtils.compress(data.getBytes());
saveToFile(compressed);
}
}
上线后 CPU 从 95% 降到了 12%。
教训:perf 是最被低估的排查工具。
perf top30 秒告诉你答案,别花两天去猜。
二、内存泄漏:OOM 不断重启,真相藏在堆外
2.1 现象
另一台服务,Java 进程每隔 3-4 天就 OOM,被 systemd 自动重启。内存监控曲线呈标准的锯齿状——缓慢上升,到顶后骤降(重启)。
$ dmesg -T | grep -i "oom"
[Mon May 5 03:17:22 2025] java invoked oom-killer: gfp_mask=0x...
[Mon May 5 03:17:22 2025] java: cgroup out of memory: kill process 23456 (java)
2.2 别盲信直觉
第一反应是 Java heap 泄漏。加了个 -XX:+HeapDumpOnOutOfMemoryError,等下次 OOM 拿 dump。
分析结果让我很困惑:heap 使用非常正常,老年代只用了 30%,GC 频率也正常。显然不是堆内泄漏。
2.3 排查堆外内存
Java 除了 heap,还有 metaspace、stack、direct buffer、JNI 分配的内存。用 top 看 RES 一直在涨:
$ top -p 23456
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
23456 work 20 0 8.5g 4.2g 12748 S 12.3 27.1 112:34.56 java
RES 4.2G 但堆只配了 2G,说明堆外占了 2G+。
2.4 pmap 查内存映射
用 pmap 看进程内存布局:
$ pmap -x 23456 | sort -k 3 -n -r | head -20
Address Kbytes RSS Dirty Mode Mapping
00007f2c00000000 1540092 1540092 1540092 rw-s- /tmp/guava-cache-1.tmp
00007f1f00000000 1049088 1049088 1049088 rw-s- /tmp/guava-cache-2.tmp
看到 guava-cache- 我瞬间明白了——某个地方用了 Guava 的 MapMaker 或 CacheBuilder,配了 softValues() 但没有限制大小,数据一直往磁盘映射文件里写。
2.5 查找代码
$ grep -r "guava" --include="*.java" | grep -i "cache"
问题代码:
// 问题代码:没有限制缓存大小,也没有过期策略
private final Cache<String, byte[]> imageCache = CacheBuilder.newBuilder()
.softValues()
.build();
softValues() 固然好,但配合 byte[] 这种大对象,加上没设 maximumSize 或 expireAfterWrite,结果就是缓存无限增长,直到占满堆外映射文件,触发 OOM。
2.6 修复
// 修复后:加限制
private final Cache<String, byte[]> imageCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.softValues()
.build();
教训:堆外内存泄漏比堆内难排查得多。遇到 OOM 先看 RES 和堆的差值,差太多就往堆外方向查。
pmap是查堆外泄漏最直接的工具。
三、排查工具箱汇总
我每次排查按这个顺序出牌:
| 阶段 | 工具 | 解决什么问题 |
|---|---|---|
| 宏观定位 | top / htop |
进程级别:CPU / 内存 / 负载 |
| 进程内分析 | perf top / perf record |
找出 CPU 热点函数 |
| 内存布局 | pmap -x |
查看 RSS 分布,定位堆外内存 |
| 系统调用 | strace -c -p |
统计系统调用频次(慎用,有性能影响) |
| 网络排查 | ss -tnp / iftop |
连接数、流量 |
| 磁盘/IO | iostat -x 1 / iotop |
IO 瓶颈确认 |
经验之谈:60% 的性能问题用
top+perf就能解决。别一上来就上复杂工具——花 3 小时配置的 APM,可能不如perf top跑 30 秒有用。
四、延伸思考
Linux 内核的 perf 子系统远比"采样热点函数"强大。 它可以追踪硬件 PMU 事件(cache miss、branch mispredict)、tracepoint(block IO、softirq),甚至动态探针(kprobe/uprobe)。这已经进入了性能工程领域,不只是"排查问题"。
容器环境给排查带来了新挑战。 你在容器里看到的 /proc/stat 可能是宿主机的全局数据,/sys/fs/cgroup 才是真正的隔离视图。千万别在容器里只看 top 就下结论。
预防比排查更重要。 这次的 Guava 缓存问题,如果能加上 JVM 参数 -XX:NativeMemoryTracking=summary,就可以用 jcmd 定期观察堆外内存变化,在 OOM 之前就发现异常曲线。事后补救永远不如事前监控。

评论已关闭!