【Win11 排查实录】32G 内存被吃掉 10G,进程只占 6G——从 RAMMap 到内核对象泄漏的完整定位过程
一、问题现象
日常使用中发现内存占用异常:
| 指标 | 数值 |
|---|---|
| 物理内存 | 32.0 GB DDR5 |
| 已使用(已压缩) | 28.9 GB(924 MB) |
| 占用率 | 93% |
| 可用 | 2.3 GB |
| 已提交 | 39.5 / 52.2 GB |
| 分页缓冲池 | 1.9 GB |
| 非分页缓冲池 | 3.6 GB |
矛盾点:任务管理器里没有任何一个进程占用突出,浏览器、编辑器加起来也不该有这么多。这是典型的"进程之和 ≠ 系统已用"场景。
二、排查方法论:为什么要先分层
很多人一上来就打开任务管理器按内存排序,这在本例中完全无效——因为泄漏根本不在用户态。
Windows 内存可以粗分为三层,排查必须自上而下:
┌─────────────────────────────────────┐
│ 应用层 Process Private │ ← 任务管理器能看到
├─────────────────────────────────────┤
│ 虚拟化层 Vmmem / WSL2 / VMware │ ← 任务管理器看得到但容易忽略
├─────────────────────────────────────┤
│ 内核层 Paged/Nonpaged Pool │ ← 任务管理器几乎看不到 ★
│ Page Table / Driver Locked │
└─────────────────────────────────────┘
核心判断动作:把所有进程内存加总,和系统报告的"已使用"对比。差额落在哪一层,就往哪一层挖。
powershell
# 进程侧总和
[math]::Round((Get-Process | Measure-Object WS -Sum).Sum/1GB, 2)
三、第一步:确认内存到底去哪了(RAMMap)
工具:SysInternals RAMMap,打开后看 Use Counts 页。
实测数据:
| 项目 | 实测值 | 正常范围 | 判定 |
|---|---|---|---|
| Process Private(全部进程) | 6.14 GB | — | ✅ 正常 |
| Page Table(页表) | 5.79 GB | 200–500 MB | 🔴 严重异常 |
| Nonpaged Pool | 2.75 GB | < 1 GB | 🔴 异常 |
| Paged Pool | 1.69 GB | < 1 GB | 🟡 偏高 |
| Driver Locked | 0.67 GB | 少量 | 🟡 偏高 |
| Mapped File | 5.15 GB | 视情况 | 🟡 偏高 |
| Metafile(NTFS 元数据) | 1.50 GB | 可回收 | ⚪ 忽略 |
结论:所有进程加起来才 6.14 GB,应用层彻底清白。内核相关合计约 11.8 GB,占 32G 的近 40%(正常应在 2–3 GB)。
关键推算:页表异常到什么程度
x64 架构下,每 4KB 页对应 8 字节 PTE,页表约占已映射内存的 0.2%。
已提交 39.7 GB × 0.2% ≈ 80 MB ← 理论值
实测 5.79 GB ← 超出 70 倍以上
这个数量级的偏差,只可能来自"大量地址空间被创建后没有被拆除"。
⚠️ 本文最大的一次误判:此处我一度认为只有虚拟机的 EPT/NPT 嵌套页表能解释,加上机器上确实有两块 VMware 虚拟网卡,于是锁定 VMware。这个判断在下一步被直接推翻。教训见第十一节。
四、第二步:定位内核池中的具体对象(poolmon)
poolmon.exe 随 Windows Driver Kit 提供,路径通常在:
C:\Program Files (x86)\Windows Kits\10\Tools\<版本>\x64\poolmon.exe
必须以管理员身份运行,进入后按键操作:
| 按键 | 作用 |
|---|---|
P |
切换 Paged / Nonpaged / 全部 |
B |
按 Bytes 降序排序 ★ |
D |
按 Diff 排序 |
Q |
退出 |
💡 默认是按 Tag 字母序排列的,不按
B排序等于什么都没看到。
排序后第一行直接给出答案:
Tag Type Allocs Frees Diff Bytes Per Alloc
Proc Nonp 160,498 2 160,496 575,067,056 3,583
数据解读
Proc 标签代表进程对象(EPROCESS)。正常 Win11 同时运行 200–400 个进程,Diff 应该就在这个量级。
实测 160,496 —— 这些进程早已退出,但内核对象没有被释放,即僵尸进程(zombie process)。
更刺眼的是 Frees = 2:开机 28 小时,只释放过 2 个进程对象。不是释放得慢,是几乎完全没释放。
交叉验证:三个标签完全对齐
| Tag | 类型 | Diff | 占用 | 含义 |
|---|---|---|---|---|
| Proc | 非分页 | 160,496 | 548 MB | 进程对象 EPROCESS |
| MiP2 | 非分页 | 160,497 | 235 MB | 每进程内存管理结构 |
| Toke | 分页 | 166,563 | 284 MB | 进程访问令牌 |
| Thre | 非分页 | 14,271 | 47 MB | 线程对象 |
| SeAt | 分页 | 681,704 | 66 MB | 安全属性 |
三个数字(16.0 万 / 16.0 万 / 16.6 万)几乎完全对齐——每个僵尸进程恰好持有一个 EPROCESS、一个 MI 结构、一个令牌。这不是巧合,是同一个泄漏的三个侧面。
页表之谜由此解开
5.79 GB ÷ 160,496 ≈ 37 KB/进程 ≈ 9 个页面
Windows 在进程退出时会拆除地址空间,但只要进程对象未被完全解引用,顶层页目录和部分页表页就必须保留(内核仍需要 DirectoryTableBase)。37 KB 正是一个僵尸进程残留页表结构的典型大小。
内存去向至此完全对上账:
页表残留 5.79 GB ← 16 万僵尸进程 × 37 KB
非分页池 2.61 GB ← Proc / MiP2 / Thre 等
分页池 1.94 GB ← Toke / SeAt 等
──────────────────────
合计约 10.3 GB 全部源自同一个泄漏
五、第三步:排除用户态句柄泄漏
僵尸进程不消失,通常是有人拿着它们的句柄。先查用户态:
powershell
Get-Process | Sort HandleCount -Desc | Select -First 15 Name,Id,HandleCount,@{n='内存MB';e={[int]($_.WorkingSet64/1MB)}} | ft -AutoSize
实测结果:
Name Id HandleCount 内存MB
---- -- ----------- ------
System 4 9494 32
explorer 347268 7809 474
lsass 2540 2606 64
dwm 381780 2435 108
Weixin 410028 2326 297
...
全部正常,最高不过 9494,所有进程加起来约 8–12 万句柄。
这个"阴性结果"反而是关键线索
它排除了用户态,把结论收紧为:
那 16 万个进程对象是被内核驱动用**对象引用(reference)**攥着的,不是句柄。
引用计数在用户态完全不可见——任务管理器、Process Explorer 都查不到。这也解释了为什么僵尸进程能悄无声息堆到 16 万。
六、第四步:抓进程创建源头
既然对象在持续增加,先算泄漏速率,判断紧急程度:
powershell
$b=(Get-CimInstance Win32_OperatingSystem).LastBootUpTime; $u=(Get-Date)-$b; "开机: $b"; "已运行: $([math]::Round($u.TotalHours,1)) 小时"; "泄漏速率: $([math]::Round(160496/$u.TotalHours)) 个/小时"
输出:
开机: 07/23/2026 12:54:15
已运行: 28.3 小时
泄漏速率: 5668 个/小时
5668 个/小时 ≈ 每秒 1.6 个进程。 正常空闲的 Win11 大约每分钟几个,高了两个数量级。
好消息是:这个速率意味着不用等,一分钟就能抓到现行。
用 WMI 事件订阅实时监控进程创建:
powershell
Register-CimIndicationEvent -ClassName Win32_ProcessStartTrace -SourceIdentifier PS1 | Out-Null; Start-Sleep 60; $e=Get-Event -SourceIdentifier PS1 -EA 0; "60秒内共创建 $($e.Count) 个进程"; $e | %{ $_.SourceEventArgs.NewEvent.ProcessName } | Group -NoElement | Sort Count -Desc | Select -First 12 | ft Count,Name -AutoSize; Unregister-Event -SourceIdentifier PS1; Get-Event -SourceIdentifier PS1 -EA 0 | Remove-Event
结果:
60秒内共创建 150 个进程
Count Name
----- ----
59 git.exe ← 主犯
59 conhost.exe ← 陪绑(每个控制台程序都会带一个)
29 taskkill.exe ← 关键线索
2 WmiApSrv.exe
1 msedge.exe
三个名字连起来看
git.exe:conhost.exe= 59 : 59 → 是某个图形界面程序在反复调起 git 命令行,每次都要新建控制台宿主taskkill.exe= 29,约为 git 的一半 → 近一半的 git 调用卡住了,被强制杀掉
这是典型的"轮询 + 超时强杀"循环。
七、第五步:定位父进程
进程名只说明"是什么",还要知道"谁调的":
powershell
Register-CimIndicationEvent -ClassName Win32_ProcessStartTrace -SourceIdentifier PS2 | Out-Null; Start-Sleep 30; $e=Get-Event -SourceIdentifier PS2 -EA 0; $e | %{ $n=$_.SourceEventArgs.NewEvent; $p=(Get-Process -Id $n.ParentProcessID -EA 0).Name; "{0} <- {1} (PID {2})" -f $n.ProcessName,$p,$n.ParentProcessID } | Group -NoElement | Sort Count -Desc | Select -First 12 | ft Count,Name -AutoSize; Unregister-Event -SourceIdentifier PS2; Get-Event -SourceIdentifier PS2 -EA 0 | Remove-Event
结果:
Count Name
----- ----
19 git.exe <- ChatGPT (PID 615016)
18 taskkill.exe <- ChatGPT (PID 615016)
元凶:ChatGPT 桌面版(Codex 智能体)。
注意这个比例:19 次调用 git,18 次被 taskkill 强杀,接近 1:1。意味着它启动的 git 进程基本没有一个正常退出过。这不是"轮询有点勤",是 git 集成功能在死循环里空转。
八、第六步:抓完整命令行,锁定根因
知道是谁调的还不够,要知道它在扫什么:
powershell
1..40 | %{ Get-CimInstance Win32_Process -Filter "Name='git.exe'" -EA 0 | select -Exp CommandLine; Start-Sleep -m 500 } | Group -NoElement | Sort Count -Desc | Select -First 5 | ft -AutoSize
结果:
30 git.exe -c core.hooksPath=NUL -c core.fsmonitor= ls-files --others --exclude-standard -z -- .VirtualBox/ ...
30 C:\Files\Git\bin\git.exe -c core.hooksPath=NUL -c core.fsmonitor= ls-files --others --exclude-standard ...
4 C:\Files\Git\bin\git.exe ... status --no-renames --ignored=matching --untracked-files=all ...
命令行里藏着全部答案
.VirtualBox/ 是典型的用户主目录下的配置文件夹,说明扫描范围是 C:\Users\<用户名>。
验证:
powershell
Test-Path "$env:USERPROFILE\.git" # 返回 True → 主目录被 git init 过
再看参数组合——恰好是 git 里最昂贵的:
| 参数 | 作用 | 后果 |
|---|---|---|
--untracked-files=all |
逐个列出未跟踪文件,不折叠目录 | 必须遍历每一个文件 |
--ignored=matching |
连被忽略的文件也要列 | .gitignore 的过滤优化失效 |
-c core.fsmonitor= |
显式禁用文件系统监视器 | 无法增量,每次全量重扫 |
-c core.hooksPath=NUL |
禁用 hooks(出于安全,合理) | — |
主目录里有什么:anaconda3\(30–50 万个小文件)、AppData\、.cache\、各种 node_modules\、.VirtualBox\,而且根本没有 .gitignore。
这个任务从设计上就不可能在 1 秒内完成。
九、根因分析:git 的"向上查找"机制
这是整件事最容易被忽略、也最值得记住的一环。
git 会一层层往上找 .git
在任何目录执行 git 命令,git 会从当前目录开始逐级向上查找 .git 文件夹,找到的第一个就是仓库根。
于是:
工作区设为 C:\Users\17564\Documents\项目\
↓ 这里没有 .git
向上找: C:\Users\17564\Documents\ 没有
继续向上: C:\Users\17564\ ← 找到 .git!
↓
git 认定仓库根 = C:\Users\17564
↓
扫描范围 = 整个用户主目录(50 万+ 文件)
所以你可能压根没把主目录设成工作区,只要工作区是主目录下的任意子目录、且它自己没有 .git,范围就会自动膨胀到整个 C:\Users\<用户名>。
主目录下有 .git 会污染其下所有子目录的 git 行为——这是本例的核心机制。
完整因果链
2024 年某次误操作,在 C:\Users\17564 执行了 git init
↓
Codex 工作区落在主目录(或其子目录)
↓
git 向上查找,认定仓库根 = 整个用户主目录
↓
每秒执行一次 git status --untracked-files=all(遍历 50 万文件)
↓
远超 1 秒超时 → taskkill /F 强杀
↓
被强杀的进程被内核驱动挂着引用,EPROCESS 无法释放
↓
28 小时累积 16 万僵尸进程
↓
页表 5.79G + 非分页池 2.6G + 分页池 1.9G ≈ 10 GB
↓
32G 内存可用仅剩 2.3G
一个讽刺的细节
git 本来有 core.untrackedCache 和 fsmonitor 两套缓存机制,专门解决大仓库扫描慢的问题。但 Codex 用 -c core.fsmonitor= 主动关掉了(大概是为保证状态绝对新鲜、不受本地配置干扰),等于自断退路。在正常小仓库里这个选择没什么代价,碰上被误 init 的主目录就成了灾难。
责任划分
| 方 | 问题 |
|---|---|
| 用户侧 | 两年前在主目录执行了 git init,平时无人触碰,一直未暴露 |
| 应用侧 | 未对仓库规模做检查;超时后无退避机制,无脑重试;强杀后不做清理 |
| 系统侧 | 疑似有内核驱动持有进程对象引用不放(Frees = 2 极不正常) |
单独任何一方都不足以造成 10 GB 损失。是两年前的一个 git init 撞上智能体的无限重试,才在 28 小时里堆出 16 万僵尸进程。
十、解决方案与验证
10.1 解除根因(改名而非删除)
cmd
:: 1. 先确认仓库里有没有需要保留的内容
git -C C:\Users\17564 log --oneline -10
git -C C:\Users\17564 remote -v
:: 2. 退出 ChatGPT 客户端,并清理残留 git 进程
taskkill /F /IM git.exe
:: 3. 改名(非破坏性,随时可回滚)
cd /d C:\Users\17564
attrib -h .git
ren .git .git_backup_20260724
:: 4. 验证
dir /a C:\Users\17564\.git*
git -C C:\Users\17564 status
期望输出:
fatal: not a git repository (or any of the parent directories): .git
⚠️ 两个注意点
- 务必用
ren而不是rd /s。若仓库启用过 Git LFS,.git\lfs里可能存着大文件的唯一副本。- 别把
.gitconfig当成仓库。它是 git 的全局配置文件(用户名、邮箱、别名),每台装了 git 的机器都有,不要动。
10.2 遇到"拒绝访问"怎么办
改名报拒绝访问 = 目录被进程占用,不是命令写错。排查顺序:
- 彻底退出 ChatGPT / Codex 客户端(含托盘)
- 关闭 VS Code / Cursor 等会读 Git 状态的编辑器
taskkill /F /IM git.exe清理残留子进程- 检查杀软实时扫描、OneDrive 同步、
SearchIndexer.exe - 精确定位:Process Explorer →
Ctrl+F→ 搜\.git→ 查看持有句柄的进程
attrib -h/attrib -s对重命名不是必需的——隐藏/系统属性不阻止 rename,关键在于解除占用。
10.3 验证效果
| 采样 | 60秒总进程数 | git.exe | taskkill.exe |
|---|---|---|---|
| 修复前 | 150 | 59 | 29 |
| 修复后第一次 | 7 | 0 | 1 |
| 修复后第二次 | 2 | 0 | 0 |
git.exe 完全归零——Codex 检测到工作区不再是 git 仓库,直接放弃轮询,连尝试都不再发起。
泄漏速率:5668 个/小时 → ≈ 0。
10.4 必须重启
⚠️ 改名不会释放已泄漏的内存。
那 10 GB 锁在内核的 16 万个僵尸进程对象上,没有任何用户态工具能清理内核对象。必须重启一次,内存才会从 93% 回落到正常的 30–40%。
正确顺序:改名 → 验证 taskkill 归零 → 重启。
10.5 加固措施(防复发)
| 措施 | 说明 |
|---|---|
| 改工作区目录 | 把 Codex 工作区指向具体项目文件夹,而非主目录。否则哪天再 git init 一次问题原地复活 |
| 关闭"完全访问权限" | 无审批的全盘读写权限,风险与收益不成比例 |
| 智能体环境改 WSL/容器 | 子进程创建在虚拟环境内,不污染宿主机内核池 |
| 定期检查主目录 | Test-Path "$env:USERPROFILE\.git" 应为 False |
10.6 遗留问题:次日复查
本次排查有一个尚未闭环的疑点:
Proc Allocs 160,498 Frees 2
这个计数是全系统的。开机 28 小时,正常启动又退出的程序不计其数,这些全都应该被回收,结果只释放了 2 个。
正常情况下,即使进程被 taskkill /F 强杀,内核也会回收其 EPROCESS。释放率接近零本身就不正常,暗示可能有内核驱动持有对象引用不放。
即:
驱动不释放引用 ← 真正的漏洞(基础病)
×
Codex 每秒拉一个 git ← 放大器(加速剂)
↓
28 小时 10 GB
拆掉放大器后,如果基础病仍在,泄漏依然存在,只是慢得多。
| 场景 | 速率 | 每月增长估算 |
|---|---|---|
| 修复前 | 5668/小时 | 几天就撑爆 |
| 修复后 + 驱动仍泄漏 | ~60/小时 | ~2.8 GB |
| 修复后 + 驱动正常 | ~60/小时 | ≈ 0 |
复查方法:正常使用 12 小时以上,重新运行 poolmon → P → B,查看 Proc 行:
- Diff 几百,Frees 与 Allocs 接近 → 彻底解决
- Diff 上万,Frees 仍是个位数 → 驱动泄漏独立存在,需单独排查
驱动嫌疑名单(按本机情况):Armoury Crate 全家桶、KLOG 标签对应的安全驱动、AMD + NVIDIA 双显卡驱动。
十一、踩坑记录
真实排查不是一条直线,把走错的路也记下来,比只记结论有用。
坑 1:被"虚拟机"假设带偏(最大的一次误判)
看到 Page Table 5.79 GB 时,我推断"超出理论值 70 倍只可能是虚拟机的 EPT/NPT 嵌套页表",加上任务管理器里确实有两块 VMware 虚拟网卡,于是锁定 VMware。
这个判断是错的。 那两块网卡只是安装后残留的虚拟适配器,不占内存。
教训:证据链看似闭合时,更要警惕。poolmon 排序后的第一行(Proc Diff = 16 万)直接推翻了假设——数据永远优先于推理。页表膨胀不止虚拟机一种成因,僵尸进程残留同样能造成,而且本例中每进程 37 KB 的数字比 EPT 假设吻合得多。
坑 2:poolmon 不按 B 排序等于白开
默认按 Tag 字母序,满屏都是 0xG3、1MPP 这类无意义小条目。必须按 P 筛类型 + 按 B 排序,大户才会浮到顶部。
坑 3:PowerShell 多行粘贴导致行序错乱
在旧版 PowerShell 控制台(尤其叠加 Anaconda 环境)里粘贴多行命令,可能出现执行顺序颠倒:
尝试除以零。
找不到"op_Subtraction"的重载,参数计数为:"2"
现象是最后一行先执行,变量还没赋值。
解决:所有命令压成单行,用 ; 连接;或改用 Windows Terminal。
行尾是 | 时 PowerShell 会显示 >> 续行提示符等待输入——此时按一次空回车即可执行,Ctrl+C 放弃。
坑 4:先重启会毁掉证据
重启确实能立刻拿回 10 GB,但泄漏证据全部消失,得重新等它累积。
正确顺序:先取证(poolmon + 速率计算 + 进程监控)→ 定位 → 修复 → 最后才重启。
坑 5:用户态查不到就以为没问题
句柄排序全部正常(最高 9494),很容易得出"没有泄漏"的结论。
实际上对象引用计数在用户态完全不可见,Process Explorer 也看不到。阴性结果不等于没问题,而是指向了更深的层次。
十二、方法论沉淀
通用排查流程
1. 分层验证:进程总和 vs 系统已用
↓ 差额在哪一层?
2. RAMMap Use Counts:确定异常项
↓ Page Table / Nonpaged Pool 异常?
3. poolmon(P + B):定位到具体 Tag
↓ 交叉验证多个 Tag 是否对齐
4. 用户态句柄排查:排除或确认
↓ 若全部正常 → 内核层
5. 计算泄漏速率:判断紧急程度 + 决定观察时长
↓
6. WMI 事件订阅:抓进程创建源头
↓
7. 追父进程 → 抓完整命令行
↓
8. 定位根因 → 修复 → 验证 → 重启 → 次日复查
几条经验
- "占用高"不等于"内存不够"。Standby、Metafile、buff/cache 都是可回收的,先看
可用而非已使用。 - 进程总和与系统已用的差额,是最有价值的单一指标。它直接决定往哪一层挖。
- 多个 Tag 相互印证比单个 Tag 可靠得多。
Proc/MiP2/Toke三个数字对齐,基本排除了误读的可能。 - 算数量级。页表理论值 80 MB vs 实测 5.79 GB;16 万 ÷ 28 小时 = 5668/小时;5.79 GB ÷ 16 万 = 37 KB。每一次除法都在收敛范围。
- 阴性结果同样是信息。用户态句柄正常,恰恰证明了问题在内核。
- 区分"导火索"和"放大器"。高频创建是导火索,不释放引用是放大器。只修一个可能只是把问题变慢,不是解决。
- 修复后必须做长周期复查。即时验证只能证明"止血成功",证明不了"没有基础病"。
附录:命令速查表
内存概览
powershell
# 进程内存 Top 15
Get-Process | Sort WS -Desc | Select -First 15 Name,Id,@{n='MB';e={[int]($_.WorkingSet64/1MB)}} | ft -AutoSize
# 所有进程内存总和(GB)
[math]::Round((Get-Process | Measure-Object WS -Sum).Sum/1GB, 2)
# 句柄数 Top 15
Get-Process | Sort HandleCount -Desc | Select -First 15 Name,Id,HandleCount | ft -AutoSize
# 系统总句柄数
(Get-Process | Measure-Object HandleCount -Sum).Sum
开机时长与泄漏速率
powershell
$b=(Get-CimInstance Win32_OperatingSystem).LastBootUpTime; $u=(Get-Date)-$b; "开机: $b"; "已运行: $([math]::Round($u.TotalHours,1)) 小时"
进程创建监控(60 秒)
powershell
Register-CimIndicationEvent -ClassName Win32_ProcessStartTrace -SourceIdentifier PS1 | Out-Null; Start-Sleep 60; $e=Get-Event -SourceIdentifier PS1 -EA 0; "60秒共创建 $($e.Count) 个进程"; $e | %{ $_.SourceEventArgs.NewEvent.ProcessName } | Group -NoElement | Sort Count -Desc | Select -First 12 | ft Count,Name -AutoSize; Unregister-Event -SourceIdentifier PS1; Get-Event -SourceIdentifier PS1 -EA 0 | Remove-Event
进程创建监控(含父进程)
powershell
Register-CimIndicationEvent -ClassName Win32_ProcessStartTrace -SourceIdentifier PS2 | Out-Null; Start-Sleep 30; $e=Get-Event -SourceIdentifier PS2 -EA 0; $e | %{ $n=$_.SourceEventArgs.NewEvent; $p=(Get-Process -Id $n.ParentProcessID -EA 0).Name; "{0} <- {1} (PID {2})" -f $n.ProcessName,$p,$n.ParentProcessID } | Group -NoElement | Sort Count -Desc | Select -First 12 | ft Count,Name -AutoSize; Unregister-Event -SourceIdentifier PS2; Get-Event -SourceIdentifier PS2 -EA 0 | Remove-Event
抓取指定进程的完整命令行
powershell
1..40 | %{ Get-CimInstance Win32_Process -Filter "Name='git.exe'" -EA 0 | select -Exp CommandLine; Start-Sleep -m 500 } | Group -NoElement | Sort Count -Desc | Select -First 5 | ft -AutoSize
非微软签名驱动清单
powershell
Get-CimInstance Win32_SystemDriver | ? State -eq 'Running' | % { $p=$_.PathName -replace '^\\\?\?\\',''; $s=Get-AuthenticodeSignature $p -EA 0; if ($s.SignerCertificate.Subject -and $s.SignerCertificate.Subject -notmatch 'Microsoft') { [PSCustomObject]@{ 驱动=$_.Name; 厂商=($s.SignerCertificate.Subject -split ',')[0] -replace 'CN=' } } } | Sort 厂商 | ft -AutoSize
Git 相关检查
powershell
Test-Path "$env:USERPROFILE\.git" # 主目录是否被 init 成仓库
where.exe git # 确认 git 来源(避免 conda 版本)
git -C $env:USERPROFILE log --oneline -10 # 查看仓库历史
git -C $env:USERPROFILE remote -v # 是否有远端备份
工具下载
| 工具 | 用途 | 来源 |
|---|---|---|
| RAMMap | 内存构成分析 | SysInternals |
| poolmon | 内核池 Tag 分析 | Windows Driver Kit |
| Process Explorer | 句柄/进程树查看 | SysInternals |
总结
| 项 | 内容 |
|---|---|
| 表象 | 32G 内存占用 93%,可用仅 2.3G |
| 误导 | 所有进程加起来只有 6.14 GB,任务管理器完全看不出问题 |
| 真相 | 16 万个僵尸进程对象,占用页表 5.79G + 内核池 4.5G ≈ 10 GB |
| 导火索 | ChatGPT 桌面版每秒调用 git 扫描整个用户主目录,超时后 taskkill /F 强杀 |
| 根因 | 两年前在 C:\Users\<用户名> 误执行 git init,git 向上查找机制导致扫描范围膨胀到 50 万文件 |
| 放大器 | 疑似内核驱动持有进程对象引用不释放(Frees = 2) |
| 解法 | 重命名主目录 .git → 验证 git.exe 归零 → 重启回收内存 → 次日复查 poolmon |
| 效果 | 60 秒进程创建数 150 → 2,git.exe 与 taskkill.exe 双双归零 |
最大的收获不是"删掉主目录的 .git"这个结论,而是那条分层排查路径——当任务管理器帮不上忙时,RAMMap 告诉你内存在哪一层,poolmon 告诉你是哪类对象,WMI 事件告诉你是谁在制造它们。
更多推荐


所有评论(0)