内存泄漏是最令人头疼的 bug 之一:应用越来越慢,风扇狂转,却不知道是哪个进程出了问题,更不知道原因何在。本文介绍一套在 macOS 上系统排查内存泄漏的方法,以 ProcXray 为核心工具。
识别症状
在拿起工具之前,先确认你面对的是真正的内存泄漏,而非正常的高占用:
- 内存持续增长(而非任务期间的短暂峰值)
- 应用空闲后内存不下降
- 系统 swap 使用量增加,即便已关闭其他应用
- 即使完成触发增长的操作后,进程也不释放内存
第一步:定位泄漏进程
打开 ProcXray,按内存降序排列。重点关注:
- 内存列数值持续攀升的进程
- 内存占用与其功能明显不符的进程(例如一个后台守护进程占用 2 GB)
ProcXray 侧边栏的实时内存图表持续更新。锁定可疑进程,观察趋势线——泄漏表现为持续向上的斜线,始终不趋于平稳。
第二步:搞清楚这个进程究竟是什么
点击可疑进程,在**概览(General)**选项卡中查看:
- 完整的可执行文件路径(当进程名模糊时非常关键,例如
node或python3) - 命令行参数(告诉你具体跑的是哪个脚本或服务器)
- 工作目录
- 父进程(告诉你是谁启动了它)
一个吃掉 3 GB 内存的后台 node 进程,在你知道它是 node /Users/me/myapp/server.js 之前毫无意义。
第三步:检查环境变量
切换到环境变量选项卡。Node.js、Python 或 Ruby 应用的内存泄漏有时源于环境配置错误:
NODE_ENV=development在生产环境中启用了大量调试中间件- ORM 配置了详细日志,将查询对象长期保留在内存中
- 缓存库配置了无限容量,从不淘汰
将所有环境变量复制为 JSON,仔细检查缓存大小、连接池限制或调试标志。
第四步:检查打开的文件描述符
服务端内存泄漏通常伴随着文件描述符泄漏——进程不断打开文件、套接字或管道,却从不关闭。切换到 ProcXray 的 Connections 选项卡,查看:
- 所有打开的文件
- 所有监听的端口
- 所有活跃的网络连接
一台本应只有 50 个文件描述符的服务器却出现了 10,000 个,是相关资源泄漏的强烈信号。
第五步:检查已加载的动态库
有时泄漏发生在某个原生库中。ProcXray 的 Dylibs 选项卡列出了进程加载的每一个动态库。将其与应用已知依赖进行对比——意外出现的库版本,或本不应存在的库,都可能解释异常行为。
第六步:复现并确认
一旦锁定了嫌疑目标(特定进程 + 代码路径),主动复现泄漏:
- 在 ProcXray 中记录该进程当前的内存占用
- 触发你怀疑导致泄漏的操作(例如上传文件、执行查询、调用 API)
- 在 ProcXray 中观察内存趋势
- 等待应用回到空闲状态
- 如果内存不回落到基线,说明你已确认泄漏路径
第七步:借助平台工具深入分析
ProcXray 告诉你什么在泄漏以及泄漏发生在哪里。要找到代码层面的原因,需要借助平台专属工具:
Swift / Objective-C 原生应用:
leaks <PID> # Apple 内置泄漏检测
malloc_history <PID> # 内存分配历史
或使用 Xcode 的 Memory Graph Debugger,以可视化调用树的形式呈现。
Node.js:
node --inspect server.js
# 然后使用 Chrome DevTools 的 Memory 选项卡,或 clinic.js
Python:
from memory_profiler import profile
@profile
def my_function():
...
常见泄漏根因
| 根因 | 信号 |
|---|---|
| 无限制缓存 | 内存随请求量线性增长 |
| 事件监听器未移除 | 内存随 UI 操作累积 |
| 原生库泄漏 | Dylibs 选项卡中出现版本异常 |
| 文件描述符未关闭 | Connections 选项卡显示大量打开文件 |
| DOM 节点被持久引用 | 即使页面跳转,浏览器进程也不释放内存 |
小结
- 在 ProcXray 中按内存排序,找出持续增长的进程
- 通过概览选项卡确认进程身份和启动方式
- 检查环境变量,寻找配置错误
- 通过 Connections 选项卡排查文件描述符泄漏
- 主动复现泄漏路径
- 将问题移交 Xcode Memory Debugger、clinic.js 或 memory_profiler 进行代码级分析
立即下载 ProcXray → — 免费使用,macOS Sonoma+。