✳相遇
导航

把 70 GB 的 Win7 虚拟机压到 41 GB:快照、清零与收缩实战

2894 字
14 分钟
把 70 GB 的 Win7 虚拟机压到 41 GB:快照、清零与收缩实战

一、起点:一个只增不减的文件#

D 盘快满了。查下来最大的一块不是代码、不是资料,而是一个 Win7 虚拟机目录:69.70 GB。

文件大小说明
Windows 7 x64.vmdk41,189 MB基线磁盘(客户机真实数据都在这里)
Windows 7 x64-000001.vmdk8,502 MB快照①的增量
Windows 7 x64-000002.vmdk11,806 MB快照②的增量(当前挂载的就是它)
Windows 7 x64-Snapshot1.vmem4,096 MB快照①的内存镜像
Windows 7 x64-Snapshot2.vmem4,096 MB快照②的内存镜像

这台机器的虚拟盘只有 80 GB(167,772,160 扇区 × 512 字节),文件却占了 69.7 GB —— 而客户机里实际用掉的空间远不到这个数。

问题的根源是一个很多人没意识到的机制:

精简置备(thin / sparse)的虚拟磁盘,客户机里写过一次的块,就永久占住了。你在 Windows 里删文件、清缓存、卸软件,宿主机上的 .vmdk 一个字都不会少。

换句话说,虚拟机磁盘文件的体积记录的是”历史高水位”,不是”当前使用量”。

二、先看清家底:三个数决定你能省多少#

动手前先确认三件事,否则后面全是白忙。

① 磁盘是精简置备还是预分配? 只有精简置备才谈得上”收缩”。

读 vmdk 头部的嵌入式描述符就能看出来(前 4 KB 就够):

Terminal window
$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.displayName20260420快照①,链条的父节点
snapshot1.displayName20260625快照②,parent = 1,挂在①后面
snapshot.current2当前运行状态位于链条最末端

顺着 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 里跑了一个清理脚本,核心动作按收益排序:

Terminal window
:: 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 bits
rd /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: 的可用空间变化 —— 这才是后面能压出多少的依据

六、第四步:清零 + 收缩(顺序不能反)#

wipe 与 shrink 的先后关系
wipe 与 shrink 的先后关系

到这里虚拟机还是 46.85 GB,但里面可能有十几个 GB 是”已删除但没回收”的块。要让它真的变小,必须:

  1. 客户机里把空闲块写成 0(wipe)
  2. 宿主机上把”全 0 的块”从 vmdk 里释放(shrink)

跳过 ① 直接跑 ②,几乎不会省空间 —— 因为从宿主机看,那些块里仍然是”有数据的”。

路线 A:一步到位(推荐)#

客户机内用 VMware Tools 自带的收缩,它会自动先清零再收缩:

Terminal window
& "C:\Program Files\VMware\VMware Tools\vmware-toolbox-cmd.exe" disk shrink C:\

前提:装了 Tools、没有快照(有快照会直接报错)、C: 至少留 10~15% 空闲。实测 42 GB 的盘跑了约 3.5 分钟。

路线 B:手动可控(更直观)#

Terminal window
# 客户机内: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 元数据(MFT、MFT、LogFile、USN 日志)系统结构,永远占着

七、WSL 的 ext4.vhdx:同样的病,不同的药#

同一天发现 WSL 的虚拟盘也是同一个毛病:

指标数值
ext4.vhdx 文件大小20,949,499,904 字节 = 19.51 GiB
/dev/sdd 里实际已用15.45 GiB
差额约 4 GiB,全是”删了但没还给宿主机”的空洞

治疗三步#

Terminal window
# ① 让 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 变成稀疏文件,以后删文件自动回收:

Terminal window
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 GB41.30 GB28.40 GB
Ubuntu(VMware,删装机残留 ISO/FLP)9.94 GB9.55 GB0.39 GB
WSL ext4.vhdx19.51 GiB17.81 GiB1.70 GiB
合计≈ 30.5 GB

耗时:快照删除与合并约 25 分钟,客户机清理约 15 分钟,shrink 3.5 分钟,WSL 压缩约 4 分钟。不用重装系统、不用迁移数据。

九、我踩过的五个坑#

  1. 以为”删一个快照就省一个 delta” 实际是折叠。delta 里独有的数据会并进父级,父级文件反而变大。真正确定能省的是 vmem 和”被覆盖的冗余块”。

  2. 不清零就直接 -k 跑完发现文件只小了 200 MB。因为空闲块里还是旧数据,VMware 无法判定它们可释放。

  3. VM 标签页没关,-k 报”文件被占用” .vmx.lck 存在就说明 Workstation 还打开着这台 VM。哪怕电源已关,也要把标签页关掉。

  4. 备份盘的”虚假容量” 顺手体检了移动硬盘:文件实际 383.9 GB,已用却是 505.7 GB —— 差了 122 GB,全是 exFAT 大簇(128 KB)在几十万个小文件上的浪费。用 exFAT 存源码级小文件,等于白扔容量。

  5. 忘了”压不掉的在用数据” 收缩前先看客户机里到底用了多少。如果 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 这种文件,建议先按第二节的三个数算一遍 —— 多数情况下,能省的空间比你以为的多,但省法比你以为的绕。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

把 70 GB 的 Win7 虚拟机压到 41 GB:快照、清零与收缩实战
https://blog.xuefei.art/posts/xuniji-shoushen/
作者
雪飞
发布于
2026-10-27
许可协议
CC BY-NC-SA 4.0
随机文章随机推荐

文章目录