Linux 性能调优实战:CPU 飙升与内存泄漏排查全指南

2026-05-09 22:49 Linux 性能调优实战:CPU 飙升与内存泄漏排查全指南已关闭评论

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 top 30 秒告诉你答案,别花两天去猜。


二、内存泄漏: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 的 MapMakerCacheBuilder,配了 softValues() 但没有限制大小,数据一直往磁盘映射文件里写。

2.5 查找代码

$ grep -r "guava" --include="*.java" | grep -i "cache"

问题代码:

// 问题代码:没有限制缓存大小,也没有过期策略
private final Cache<String, byte[]> imageCache = CacheBuilder.newBuilder()
    .softValues()
    .build();

softValues() 固然好,但配合 byte[] 这种大对象,加上没设 maximumSizeexpireAfterWrite,结果就是缓存无限增长,直到占满堆外映射文件,触发 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 VM.native_memory 定期观察堆外内存变化,在 OOM 之前就发现异常曲线。事后补救永远不如事前监控。

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4132.html ,谢谢支持!

资源分享

分类:Android 标签:
Open Claw Anthropic 模型配置指南 Open Claw Anthropic 模型配置
Android Studio如何使用桌面版GitHub管理项目? Android Studio如何使用桌面版
Android学习笔记一:Java类加载过程 Android学习笔记一:Java类加载
使用Kotlin实现设计模式中的工厂模式 使用Kotlin实现设计模式中的工厂

评论已关闭!