把 70 GB 的 Win7 虚拟机压到 41 GB:快照、清零与收缩实战
一、起点:一个只增不减的文件
D 盘快满了。查下来最大的一块不是代码、不是资料,而是一个 Win7 虚拟机目录:69.70 GB。
| 文件 | 大小 | 说明 |
|---|---|---|
Windows 7 x64.vmdk | 41,189 MB | 基线磁盘(客户机真实数据都在这里) |
Windows 7 x64-000001.vmdk | 8,502 MB | 快照①的增量 |
Windows 7 x64-000002.vmdk | 11,806 MB | 快照②的增量(当前挂载的就是它) |
Windows 7 x64-Snapshot1.vmem | 4,096 MB | 快照①的内存镜像 |
Windows 7 x64-Snapshot2.vmem | 4,096 MB | 快照②的内存镜像 |
这台机器的虚拟盘只有 80 GB(167,772,160 扇区 × 512 字节),文件却占了 69.7 GB —— 而客户机里实际用掉的空间远不到这个数。
问题的根源是一个很多人没意识到的机制:
精简置备(thin / sparse)的虚拟磁盘,客户机里写过一次的块,就永久占住了。你在 Windows 里删文件、清缓存、卸软件,宿主机上的 .vmdk 一个字都不会少。
换句话说,虚拟机磁盘文件的体积记录的是”历史高水位”,不是”当前使用量”。
二、先看清家底:三个数决定你能省多少
动手前先确认三件事,否则后面全是白忙。
① 磁盘是精简置备还是预分配? 只有精简置备才谈得上”收缩”。
读 vmdk 头部的嵌入式描述符就能看出来(前 4 KB 就够):
$fp = 'D:\vms\Win7 X64\Windows 7 x64.vmdk'$fs = [IO.File]::OpenRead($fp)$buf = New-Object byte[] 4096$null = $fs.Read($buf, 0, 4096); $fs.Close()$txt = [Text.Encoding]::ASCII.GetString($buf)
if ($txt -match 'createType="([^"]+)"') { 'createType = ' + $Matches[1] }if ($txt -match 'RW (\d+) SPARSE') { '容量 = ' + [math]::Round([int64]$Matches[1]*512/1GB,1) + ' GB' }输出 monolithicSparse = 精简置备,可以收缩;如果是 flat / preallocated,得先转换(vmware-vdiskmanager -r)。
② 快照链有多长? 读 .vmsd 文件,snapshot*.parent 字段就是链条的父子关系:
.vmsd 字段 | 值 | 含义 |
|---|---|---|
snapshot0.displayName | 20260420 | 快照①,链条的父节点 |
snapshot1.displayName | 20260625 | 快照②,parent = 1,挂在①后面 |
snapshot.current | 2 | 当前运行状态位于链条最末端 |
顺着
parent一路往上找,就能还原出base ← delta1 ← delta2的完整链。
③ 有没有 vmem? .vmem 是”带内存的快照”存下来的内存镜像,大小≈虚拟机内存。开机状态下打的快照,每个都会白占一份内存大小的空间。 这台 4 GB 内存的机器,两个快照就是 8 GB。
三、第一步:先扔掉 8 GB 的内存镜像
.vmem 唯一的作用是”精确定点恢复那一刻的运行现场”。如果只需要”回到那天的磁盘状态”(冷启动引导),这 8 GB 毫无价值。
两种方式让它消失:
- 恢复快照后正常关机:从挂起状态启动 → 客户机里 Shut Down → 挂起内存被丢弃,vmem 自动删除,快照本身仍在(变成”仅磁盘状态”快照)
- 直接删除快照:连内存镜像带增量链一起处理
这一步的收益是确定的、立刻可见的:8 GB。
四、第二步:删快照 ≠ 省下一整个 delta
这是最容易误判的地方。假设链是 base ← delta1 ← delta2:
| 操作 | 实际发生 | 空间变化 |
|---|---|---|
| 删 delta2(当前快照) | delta2 的独有块合并进 delta1 | 省掉 delta2 里”被覆盖的旧版本块”,不是省 11.8 GB |
| 删 delta1(父快照) | delta1 合并进 base | 同理,base 会变大 |
| 全删 | 全部折叠进 base | 增量文件消失,总大小 ≈ 基线 + 独有数据 |
所以”删快照”的真实收益是:vmem(确定)+ 冗余块(不确定)+ 后续能收缩的上限(很大)。
这台机器的实测路径:
| 操作 | 目录大小变化 | 明细 |
|---|---|---|
| 删快照①(20260420) | 69.70 GB → 57.92 GB | −11.78 GB(4 GB vmem + 8.5 GB delta 折叠) |
| 删快照②(20260625) | 57.92 GB → 45.76 GB | −12.16 GB(4 GB vmem + 12.1 GB delta 折叠) |
| 结果 | 基线 vmdk 41,189 → 46,853 MB | 它变大了,这是正常的 |
到这里累计已经省下 23.9 GB,而客户机里的文件一个都没动。
五、第三步:客户机内部清理(这一步决定天花板)
前两步只是”清理账本”,真正能把文件压小的前提是客户机里有大量可回收的空闲块。
我在 Win7 里跑了一个清理脚本,核心动作按收益排序:
:: 1) 磁盘清理(含系统文件),注册全部项目后静默执行for %%K in ("Active Setup Temp Folders" "Downloaded Program Files" ^ "Internet Cache Files" "Previous Installations" "Recycle Bin" ^ "Setup Log Files" "System error memory dump files" ^ "Temporary Files" "Thumbnail Cache" "Windows Upgrade Log Files") do ( reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\%%~K" ^ /v StateFlags0777 /t REG_DWORD /d 2 /f >nul 2>&1)cleanmgr /sagerun:777
:: 2) Windows Update 缓存net stop wuauserv & net stop bitsrd /s /q "%SystemRoot%\SoftwareDistribution\Download"md "%SystemRoot%\SoftwareDistribution\Download"net start wuauserv & net start bits
:: 3) 临时文件 / 缩略图 / 转储del /f /s /q "%SystemRoot%\Temp\*.*"del /f /s /q "%TEMP%\*.*"del /f /s /q "%LOCALAPPDATA%\Microsoft\Windows\Explorer\thumbcache_*.db"del /f /s /q "%SystemRoot%\Prefetch\*.pf"
:: 4) 系统还原点(保留功能、删旧记录,常见能省 5~15 GB)vssadmin delete shadows /all /quiet
:: 5) 关休眠(释放 hiberfil.sys ≈ 内存大小)powercfg -h off几个注意点:
cleanmgr的StateFlagsNNNN后缀必须和/sagerun:NNNN的数字一致,否则静默执行等于没跑vssadmin delete shadows只删还原点,不关闭还原功能,属于低风险高收益- Win7 没有
DISM /StartComponentCleanup,WinSxS能清的部分走cleanmgr里的「以前的 Windows 安装」 - 脚本跑完一定要看
C:的可用空间变化 —— 这才是后面能压出多少的依据
六、第四步:清零 + 收缩(顺序不能反)
到这里虚拟机还是 46.85 GB,但里面可能有十几个 GB 是”已删除但没回收”的块。要让它真的变小,必须:
- 客户机里把空闲块写成 0(wipe)
- 宿主机上把”全 0 的块”从 vmdk 里释放(shrink)
跳过 ① 直接跑 ②,几乎不会省空间 —— 因为从宿主机看,那些块里仍然是”有数据的”。
路线 A:一步到位(推荐)
客户机内用 VMware Tools 自带的收缩,它会自动先清零再收缩:
& "C:\Program Files\VMware\VMware Tools\vmware-toolbox-cmd.exe" disk shrink C:\前提:装了 Tools、没有快照(有快照会直接报错)、C: 至少留 10~15% 空闲。实测 42 GB 的盘跑了约 3.5 分钟。
路线 B:手动可控(更直观)
# 客户机内:Sysinternals sdelete 清零空闲块sdelete64.exe -z C:
# 宿主机:先关掉 Workstation 里这台 VM 的标签页(否则 .vmx.lck 锁会让 -k 失败)& 'C:\Program Files (x86)\VMware\VMware Workstation\vmware-vdiskmanager.exe' ` -d "D:\vms\Win7 X64\Windows 7 x64.vmdk" # 可选:整理碎片& 'C:\Program Files (x86)\VMware\VMware Workstation\vmware-vdiskmanager.exe' ` -k "D:\vms\Win7 X64\Windows 7 x64.vmdk" # 收缩实测结果:46,853 MB → 42,286 MB(-4.57 GB),目录从 45.76 GB 降到 41.30 GB。
为什么只省了 4.57 GB?因为这台 Win7 里装的 VS / IAR / Keil 那套工具链本身就占 30 GB 左右。收缩只能回收”已删除的空闲块”,回收不了”确实在用”的数据。
压不掉的两块(提前知道少踩坑)
| 项目 | 为什么压不掉 |
|---|---|
pagefile.sys | 属于”在用”状态,内容不是 0;想回收只能先在系统里调小页面文件,重启后再 shrink 一次 |
| NTFS 元数据(LogFile、USN 日志) | 系统结构,永远占着 |
七、WSL 的 ext4.vhdx:同样的病,不同的药
同一天发现 WSL 的虚拟盘也是同一个毛病:
| 指标 | 数值 |
|---|---|
ext4.vhdx 文件大小 | 20,949,499,904 字节 = 19.51 GiB |
/dev/sdd 里实际已用 | 15.45 GiB |
| 差额 | 约 4 GiB,全是”删了但没还给宿主机”的空洞 |
治疗三步
# ① 让 Linux 把空闲块标记为 discard(同时归零)wsl -d Ubuntu-24.04-FuXi -e sh -c 'fstrim -av; sync'# → /: 990.6 GiB (1063677743104 bytes) trimmed on /dev/sdd
# ② 关闭 WSL(vhdx 被独占锁定,运行中无法压缩)wsl --shutdown
# ③ 用 diskpart 压缩 vhdx(需管理员)diskpart select vdisk file="D:\wsl\Ubuntu-24.04\ext4.vhdx" compact vdisk exit
diskpart里那三行需要逐行输入;也可以把它们写进compact.txt,用diskpart /s compact.txt一次执行。
实测:19.51 GiB → 17.81 GiB(-1.70 GiB),之后启动 WSL 验证 df -h 正常、数据完好。
关于 --set-sparse:被安全策略挡住了
WSL 2.6+ 提供了更优雅的方案 —— 让 vhdx 变成稀疏文件,以后删文件自动回收:
wsl --manage Ubuntu-24.04-FuXi --set-sparse true但我这台机器上直接报错:
由于存在安全限制,不允许使用稀疏 VHD。如需允许,请运行:
wsl.exe --manage <Distro> --set-sparse true --allow-unsafe
--allow-unsafe 的含义是:如果这个 VHD 同时被别的进程写入,稀疏块的元数据可能错乱,导致虚拟盘损坏。为了 1.7 GB 冒这个风险不值当,所以最终选择了 fstrim + compact 这条”老办法”。
顺带一提:
diskpart compact vdisk对非稀疏的 vhdx 效果最好。如果先设成了 sparse,反而不容易压,必要时得先--set-sparse false再压。
八、账本:一天做完的收益
| 对象 | 优化前 | 优化后 | 释放 |
|---|---|---|---|
| Win7 X64(VMware) | 69.70 GB | 41.30 GB | 28.40 GB |
| Ubuntu(VMware,删装机残留 ISO/FLP) | 9.94 GB | 9.55 GB | 0.39 GB |
| WSL ext4.vhdx | 19.51 GiB | 17.81 GiB | 1.70 GiB |
| 合计 | ≈ 30.5 GB |
耗时:快照删除与合并约 25 分钟,客户机清理约 15 分钟,shrink 3.5 分钟,WSL 压缩约 4 分钟。不用重装系统、不用迁移数据。
九、我踩过的五个坑
-
以为”删一个快照就省一个 delta” 实际是折叠。delta 里独有的数据会并进父级,父级文件反而变大。真正确定能省的是 vmem 和”被覆盖的冗余块”。
-
不清零就直接
-k跑完发现文件只小了 200 MB。因为空闲块里还是旧数据,VMware 无法判定它们可释放。 -
VM 标签页没关,
-k报”文件被占用”.vmx.lck存在就说明 Workstation 还打开着这台 VM。哪怕电源已关,也要把标签页关掉。 -
备份盘的”虚假容量” 顺手体检了移动硬盘:文件实际 383.9 GB,已用却是 505.7 GB —— 差了 122 GB,全是 exFAT 大簇(128 KB)在几十万个小文件上的浪费。用 exFAT 存源码级小文件,等于白扔容量。
-
忘了”压不掉的在用数据” 收缩前先看客户机里到底用了多少。如果 40 GB 里 35 GB 是真实安装的软件,最多只能压出 5 GB —— 这时候该做的是卸载/迁移,而不是反复 shrink。
十、可复用的检查清单
| 阶段 | 检查项 | 命令/位置 |
|---|---|---|
| 术前 | 磁盘类型是精简置备? | vmdk 头部 createType |
| 术前 | 有没有快照?各是谁的父? | .vmsd 的 parent |
| 术前 | 每个 vmem 多大? | 目录里 *.vmem |
| 术前 | 客户机 C: 已用多少? | 磁盘属性 / wmic logicaldisk |
| 清理 | 磁盘清理 + 还原点 + 休眠 | 见第五节脚本 |
| 收缩 | 先 wipe 再 shrink,顺序不能反 | vmware-toolbox-cmd disk shrink |
| 收尾 | 宿主机文件大小变化 | Get-Item *.vmdk | % Length |
| 收尾 | 启动验证 + 数据可用 | 开机跑一遍关键软件 |
| 长期 | 快照只在关机态打 | 避免 4 GB/次的 vmem |
| 长期 | 排除杀软对 vmdk 的实时扫描 | 性能 + 避免 EPERM |
写在最后
虚拟机瘦身的本质不是”压缩”,而是把”宿主机以为被占用”和”客户机实际在用”这两笔账对齐。
对齐的方式只有两条:把不用的东西真的删掉(客户机内清理),以及把删掉之后留下的空洞明确告诉宿主机(wipe + shrink)。中间所有关于快照的纠结,都是在这个基础上做减法。
如果你的虚拟机也已经跑了几年、目录里出现 -000001、-000002 这种文件,建议先按第二节的三个数算一遍 —— 多数情况下,能省的空间比你以为的多,但省法比你以为的绕。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!







