进程和诊断工具学习笔记(8.15):实战案例|内存狂涨 / 句柄泄漏怎么查?用 VMMap + Handle + ListDLLs 三步定位 - 指南

进程和诊断工具学习笔记(8.15):实战案例|内存狂涨 / 句柄泄漏怎么查?用 VMMap + Handle + ListDLLs 三步定位

适用人群:

  • 运维/值班同学:被喊“有服务器进程占满内存,快救火”的那种
  • 开发同学:有人说你程序“有内存泄漏/资源泄漏”,但没人能拿出证据
  • 安全/应急同学:怀疑进程被异常注入、行为异常

一句话目标:
这篇是落地排障 SOP,教你怎么在一台正在“疯狂吃内存 / 吃句柄”的 Windows 机器上,快速判断问题根因在哪一类资源,并把“猜测”变成“证明”。


一、常见报警长什么样?

你可能经常遇到这些画面:

  • 任务管理器里某个进程(比如 xxx.exe)内存占用冲到几 GB,甚至把系统拖死
  • 系统报 “资源不足,无法创建新窗口 / 打不开文件 / 打不开句柄”
  • 程序运行几小时后越来越慢,最后直接卡死,但 CPU 不高

这些现象其实往往不是一回事,可能是:

  1. 内存泄漏
    程序不停申请堆内存 / 映射内存 / GDI 对象,但不释放。

  2. 句柄泄漏(Handle leak)
    程序不停打开文件句柄、注册表句柄、事件、互斥量、管道等等,但忘了关闭。
    句柄用光,系统会开始各种报错,界面可能打不开,但未必“看上去内存特别高”。

  3. 模块注入 / 第三方 DLL 乱搞
    某些外部 DLL 插进你的进程,疯狂分配对象(常见:劣质插件、劫持、甚至恶意模块)。

我们要做的,就是把这三种情况区分清楚,给出证据链。


二、排查总流程(建议背下来)

这个流程你完全可以做成公司内部“内存/句柄告警应急手册”。

Step 0:先定位问题进程是谁

  • 用任务管理器 / tasklist,找出占用异常的进程名称和 PID(进程 ID)。
  • 后续所有工具都建议基于 PID 操作,别光靠名字,特别是同名多实例的场景(比如 w3wp.exe / java.exe / chrome.exe 这种多进程模型)。

Step 1:用 VMMap 看“它的内存到底花在哪”

VMMap = 可视化内存剖析工具,图形化告诉你:

  • 堆?
  • 映射文件?
  • 私有提交?
  • GDI / USER?
  • 还是某块超大的保留区没释放?

操作思路简化版:

  1. 打开 VMMap,选择目标 PID。

  2. 重点看这几块:

    • Private Bytes / Private Data:应用真正独占、由它自己分配的内存。典型“堆泄漏”就是这块一直涨。
    • Mapped File:如果巨高,说明它映射了大量文件/缓存(浏览器、数据库缓存很常见)。
    • Heap、Stack、.NET GC Heap 等分类,谁在涨就是嫌疑人。
  3. 打快照(Snapshot),过一段时间再打第二个快照,比较差异。

    • 哪一类内存一直在涨 → 把方向锁定。

怎么用这个结果?

  • 如果是 “Heap / Private” 疯涨 → 多半是程序本身在泄漏(业务 Bug)。
  • 如果是 “Mapped File” 疯涨 → 可能是大量日志/缓存映射,或三方库/驱动搞的事,不一定是内存泄漏。
  • 如果是 GDI / USER 对象狂涨(GUI 程序常见)→ 可能是控件没释放、窗体句柄没销毁。

到这一步,你可以很自信地说一句:“不是系统锅,是这个进程的堆在涨,而且是私有提交内存持续上涨,怀疑用户态泄漏,需要开发排查分配逻辑。”
这比“好像内存有点高”有说服力太多了。


Step 2:用 Handle.exe 观察句柄数量是否爆炸

Handle.exe = Sysinternals 的句柄查看/搜索工具。
它能列出进程里“正在占着不放的每一个句柄”,包括:

  • 文件
  • 注册表键
  • 互斥锁 / 事件 / 信号量
  • 命名管道
  • Job / Section / Thread / Token 等内核对象

典型命令用法:

handle.exe -p <PID>

你会看到一长串东西,形如:

1234: File  (RW-)   C:\Logs\service.log
1234: Mutant        \Sessions\1\BaseNamedObjects\Global\MyLock_0421
1234: Key           HKCU\Software\Vendor\Foo\Bar
...

重点关注两件事:

  1. 句柄总数是否离谱

    • 正常业务进程可能是几百到几千。
    • 如果你看到几万甚至几十万句柄,基本可以认定是句柄泄漏。
    • 这会导致“系统资源不足,请关闭一些程序”之类奇怪报错。
  2. 有没有某一类句柄在疯狂重复出现

    • 同一个文件路径重复上万次?怀疑文件句柄没 CloseHandle。
    • 同一个注册表键重复上万次?怀疑配置读取没关句柄。
    • 同一类 “Mutant/Mutex/Section” 对象暴涨?怀疑线程同步逻辑写崩了。

配合技巧:

  • 多次采样,比如隔 30 秒跑一次 handle.exe -p <PID>,把输出重定向到文本里,对比数量递增的对象类型。
  • 这一步拿到的内容,你可以甩给开发:“你们看,C:\Logs\access.log 这个文件句柄从 5 分钟前的 2000 涨到了现在的 58000,还没释放,这不是系统,是你们的写日志逻辑。”

进阶:搜索特定资源占用者

如果你知道哪个文件删不掉 / 哪个注册表项被锁着,可以反查是谁占着它:

handle.exe C:\Logs\access.log

它会告诉你“哪个 PID 正拿着它不放”。这在“文件被占用无法替换 DLL / 无法删除旧日志”时超好用。


Step 3:用 ListDLLs 确认这个进程到底被谁‘动过手脚’

为什么还要看 DLL?

因为有些时候,泄漏并不是主程序自己的锅,而是“别人塞进去的 DLL 在搞事情”:

  • 劣质杀毒/加密狗/打印驱动插件
  • 广告类/劫持类组件注入浏览器、Office、IM 客户端
  • Hook API、抓流量、外挂式监控模块

Listdlls.exe -p <PID> 会列出该进程中所有已加载的模块(DLL),包含路径、基址、版本信息、签名信息(我们通常关心是不是微软签名/厂商签名)。

你要看的是:

  1. 有没有明显不属于这个进程的奇怪 DLL

    • 例如一个业务后端服务里突然出现浏览器插件 DLL、广告相关 DLL、某国产驱动厂商的监控 DLL。那就很可疑。
  2. 有没有“同名不同路径”的 DLL 劫持

    • 比如本来应该加载 C:\Windows\System32\version.dll,结果进程里出现了 C:\Temp\version.dll
      这可能是典型 DLL 劫持/侧载,甚至是恶意持久化。

为什么这一步很关键?

  • 如果句柄泄漏其实是某个注入 DLL 的锅(比如它不断打开日志文件/管道却不释放),那开发团队是没法自己修的,你得找对应厂商或考虑卸载该模块。
  • 如果你排查的是安全事件,这一步直接告诉你“有没有未知模块常驻内存”。

三、把三步串成一份“故障证据链”

组合起来看:

  1. VMMap

    • 证明:哪类内存在涨(堆?文件映射?GDI?私有提交?)
    • 说明问题属于“内存泄漏 / 正常缓存膨胀 / 图形对象泄漏 / 句柄耗尽引发的资源紧张”等哪一类。
  2. Handle

    • 证明:句柄是不是在爆炸式上涨?是文件型还是同步对象型?
    • 锁定具体资源名字,能定位到代码路径或第三方库。
  3. ListDLLs

    • 证明:这个进程里有哪些“外来的东西”?
    • 是否存在第三方/异常模块,谁可能是罪魁祸首?

你拿这三份截图/输出,和时间点(“10:02 vs 10:07”),基本就能给出一份严谨的初步分析报告,而不是“应该是内存泄漏吧你们看一下”。


四、现场排查的注意事项(血泪经验)

1. 一定要记录时间线
别只截一张图。要能说明“5 分钟内从 3GB 涨到 5GB”,或者“句柄从 4 万涨到 9 万”。趋势 = 说服力。

2. 不要一上来就 Kill
很多同学一发现进程异常就先 taskkill /f /pid xxx
问题是:你一杀,证据就没了。
正确顺序应是:采样(VMMap快照 / Handle输出 / ListDLLs列表)→ 备份 → 才考虑重启/回收。

3. 权限问题

  • Handle / ListDLLs / VMMap 读取别的进程时往往需要管理员权限(尤其是系统服务、别的用户会话下的进程)。
  • UAC、EDR 可能会拦截。必要时要在变更/应急流程内申请。

4. 线上 or 复现机?
线上敏感、核心产线进程不一定能随便 attach 调试器或长时间抓取。
如果能复现,最好在影子环境 / 灰度环境里跑同一版本的进程,重现增长趋势,再抓。


五、把它固化成团队 SOP(模板拿走就能用)

下面是一份可直接落地的“资源暴涨排查模板”,建议放到团队 Confluence / 飞书知识库里:

【告警时间】
YYYY-MM-DD HH:MM
【告警描述】
- 服务器:HOSTNAME / IP
- 进程名 + PID:
- 现象:
  - 内存占用:____ MB -> ____ MB(____分钟内)
  - 句柄数:____ -> ____(采样间隔:____秒)
【VMMap 结果】
- 哪一类内存在持续上涨:
  - [ ] Private Bytes
  - [ ] Mapped File
  - [ ] Heap
  - [ ] GDI / USER
  - [ ] 其他:_____
- 增长速率 & 时间点对比截图
【Handle 结果】
- 句柄总数:____
- 疑似泄漏的对象类型:
  - 文件:______(示例路径:_____)
  - 注册表:_____
  - 同步对象(Mutex/Event/...):_____
- 是否持续增长:是 / 否
【ListDLLs 结果】
- 异常模块:
  - 模块名 / 路径 / 签名者:
  - 是否属于业务自身:
  - 怀疑的第三方/驱动/安全软件:
【初步结论】
- [ ] 疑似业务自身内存泄漏(堆/私有提交暴涨)
- [ ] 疑似日志/IO句柄泄漏(Handle 输出表明同一路径句柄激增)
- [ ] 疑似第三方 DLL 注入导致资源占用
- [ ] 其他:_____
【建议动作】
- [ ] 通知对应服务Owner排查内存分配逻辑
- [ ] 临时重启服务缓解(窗口期:____)
- [ ] 下线/禁用可疑第三方插件
- [ ] 触发进一步取证(内存转储 / ProcDump / LiveKd)

你会发现:

  • 这个模板不仅能帮你写“事后报告”,也能让任何接锅同事迅速复盘。
  • 长期看,它还能变成 SLA/值班守则的一部分。

六、总结一下(背下来就够用了)

  1. 看内存用 VMMap,不要只看任务管理器
    任务管理器只告诉你“高”,VMMap告诉你“为什么高”。

  2. 看资源泄漏用 Handle,而不是猜
    Handle 能告诉你“谁开的文件没关”“谁拿着互斥锁不放”。

  3. 看外来模块用 ListDLLs
    很多“离谱资源占用”其实是别的 DLL 干的,尤其是各类插件、Hook、加密狗、广告/安全代理。

  4. 要有时间线对比 + 证据截图
    没有趋势就说不清“这是泄漏还是正常缓存”。

  5. 先取证,再回收
    别一上来就 kill 进程,否则你自己成背锅侠:因为你“毁灭了唯一能解释问题的现场”。


下一篇(8.16)我会专门讲**“LiveKd + 内核态视角”**:当用户态工具(VMMap/Handle)都查不出根因时,怎么对正在跑的机器做“活体内核调试”,包括符号配置、Hyper-V 客户端调试思路、以及怎么在不蓝屏的前提下拉到现场证据。

posted @ 2025-12-15 16:53  clnchanpin  阅读(52)  评论(0)    收藏  举报