起因
926GB 的磁盘用了 178GB,剩 724GB——乍看不紧张,但 ~/Library 一家就占了 38GB,加上散落在各处的开发构建产物、包管理器缓存、AI 工具会话,怎么看怎么像定时炸弹。正好前阵子写过 Homebrew 每周自动更新,这次照同一套路:先人工摸底清理,再把可重复的部分脚本化 + 定时化。
第一步:逐层 du,画出占用地图
清理的第一原则是先测量再动手。几条命令层层下钻:
df -h /System/Volumes/Data
du -sh ~/.* ~/Library 2>/dev/null | sort -hr | head -n 50
du -sh ~/Library/* ~/Library/Caches/* 2>/dev/null | sort -hr | head -n 30
摸出来的大头(按可清理性分三类):
可安全自动清(缓存/日志/旧版本,约 20GB):
| 位置 | 大小 | 性质 |
|---|---|---|
~/Library/Caches/Homebrew/downloads | 6.1GB | brew 下载缓存 |
~/.npm/_cacache | 4.8GB | npm 缓存 |
~/.codex/sessions | 5.7GB | Codex 历史会话 |
~/Library/Caches/JetBrains/IntelliJIdea2026.1 | 3.4GB | IDE 旧版本缓存(当前 2026.2) |
~/Library/Caches/ms-playwright | 1.1GB | 旧浏览器内核 |
~/Library/Logs/JetBrains | 764MB | IDE 日志 |
需手动确认(约 14GB):
| 位置 | 大小 | 备注 |
|---|---|---|
~/VirtualBox VMs/basic_bak | 5.4GB | 与 basic 重复的整机备份 |
~/Downloads/ubuntu-*.iso | 2.8GB | 用完即删的典型 |
~/Library/pnpm/store | 5.0GB | 可删一次,后续 prune 维持 |
~/Documents/某业务流程图.zip | 408MB | 已有同名解压目录,纯重复 |
碰都别碰: ~/.m2/repository(4.3GB,删了重下更慢)、项目 target/build(默认只报告)、各 App 的 Application Support 数据。
第二步:手动清理,一次释放 21GB
按”先大后小、先确认后删除”的顺序执行。虚拟机备份删前先确认 basic 目录完好;JetBrains 旧版三个位置(Caches / Application Support / Logs)要一起清,只清一处不清爽:
rm -rf ~/"VirtualBox VMs"/basic_bak \
~/Downloads/ubuntu-24.04.4-live-server-arm64.iso \
~/Downloads/WeChatMac_4.1.13.dmg \
~/Documents/某业务流程图.zip \
~/Library/Caches/JetBrains/IntelliJIdea2026.1 \
~/Library/Application\ Support/JetBrains/IntelliJIdea2026.1 \
~/Library/Logs/JetBrains/IntelliJIdea2026.1 \
~/Library/pnpm/store
brew cleanup -s # 释放 3.7GB
npm cache clean --force # 释放 4.8GB
结果:磁盘已用 184GB → 163GB。pnpm store 删后用 corepack pnpm --version 验证过(本机 pnpm 是 corepack 托管的 12.5.1),下次安装自动重建。
第三步:写成 9 步清理脚本
手动清理不可重复,把规律性的部分写成 ~/scripts/system-cleanup.sh,分 9 步,每一步都是”条件触发 + 可再生”:
- Homebrew 缓存 →
brew cleanup -s+autoremove - npm/pnpm 缓存 → npm 超 1GB 才清,pnpm 只
store prune - JetBrains 旧版 → 自动识别版本号最大的保留,其余删除
- Codex/Claude 会话 → 默认关闭,
CLEAN_AI_SESSIONS=1才删(见踩坑 1) - Maven/Gradle → 只删
*.lastUpdated失败标记,不动仓库主体 - 通用日志 →
*.log按 14/30 天滚动 - 回收站/会话 → 只清 30 天前的
- 项目构建产物 → 默认关闭,
CLEAN_BUILD_OUTPUTS=1才删 target/build/dist - 大户报告 → iso/dmg、虚拟机、仓库主体只打印占用,不删
工程化细节和 brew 脚本同一套:mkdir 原子锁防重入、日志超 5MB 轮转、删除前后统计释放量。外加两个安全开关:
DRY_RUN=1 bash ~/scripts/system-cleanup.sh # 预演,只报告不删除
CLEAN_BUILD_OUTPUTS=1 bash ~/scripts/system-cleanup.sh # 连带清构建产物
第四步:launchd 每周日自动执行
定时器 ~/Library/LaunchAgents/com.lucas.system-cleanup.plist,每周日 03:00(特意避开周一 09:00 的 brew 任务):
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
注册验证三件套:
plutil -lint ~/Library/LaunchAgents/com.lucas.system-cleanup.plist # OK
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.lucas.system-cleanup.plist
DRY_RUN=1 bash ~/scripts/system-cleanup.sh # 先预演再上线
脚本是热加载的——以后改脚本不用动定时器,下次执行自动用新版。
第五步:终端工具链统一迁移到 brew
清理磁盘时顺手盘了终端工具的家底,发现比磁盘还乱:同一个工具多处安装、有的已损坏、node 版本管理器(nvm)根本没接入 shell。原则是能进 brew 的都进 brew,这样周一的自动更新流水线顺手就管了:
| 工具 | 之前 | 之后 |
|---|---|---|
codex | nvm 里的 symlink 目标失踪,全终端不可用 | brew 版 node 的 npm 全局重装,0.155.1 ✅ |
opencode | pnpm 版已坏 + 官方版两处重复 | 都不要了,三处全删 |
| node 版本管理 | 孤儿 nvm(.zshrc 无加载行)+ brew node@24 | 删 nvm,改用 brew 多版本:node@24(主)+ node@22(LTS),brew unlink/link 切换 |
git | Apple Git 2.54.0 | brew Git 2.55.0(配置与凭证助手通用,无缝) |
| 新增 | — | gh(GitHub CLI) |
python | 3.13 + 3.14 并存 | 删 3.13(无反向依赖),留 3.14 |
openjdk27 | brew 版(391MB) | 删,见第六步 |
| PATH | /opt/homebrew/bin 重复 4 次 | .zshrc 守卫 + 末尾全局去重,多次 source 零膨胀 |
其中 brew install codex 装上的是个残的 Cask(shim 指向不存在的二进制),卸掉改走 npm CLI——别迷信包名,装完一定要 which + --version 双验证。
第六步:maven 与 IDEA 全栈统一 Oracle JDK25
brew 的 maven 已是最新 3.9.16,无需升级。真正的统一对象是 JDK:本机有 Oracle 25、微软 21、JetBrains 自带 JBR 17/21 四套,而项目只用到 11/17/25 三档。由于 JDK25 可向下编译(release 参数),构建统一用 JDK25 是可行的:
# .zshrc:终端 + maven 默认 JDK25
if [ -d "${HOME}/Library/Java/JavaVirtualMachines/openjdk-25.0.2/Contents/Home" ]; then
export JAVA_HOME="${HOME}/Library/Java/JavaVirtualMachines/openjdk-25.0.2/Contents/Home"
fi
brew uninstall --ignore-dependencies openjdk删掉 brew JDK27(391MB)。--ignore-dependencies是因为 maven 声明依赖它,但实测 maven 随JAVA_HOME走 JDK25 完全正常——注意将来brew upgrade maven可能会把它装回来,届时再卸一次即可。- 微软 JDK21 删除(336MB,JDK25 可覆盖其编译目标);JBR 是 IDE 自带运行时,保留。
- IDEA 项目 SDK 统一:扫全部 37 个项目的
.idea/misc.xml,6 个异类(指向 JBR、重复命名、缺失)改成openjdk-25;jdk.table.xml里删掉指向已删 JDK 的死条目和重复命名。language level 保持不动(JDK_17 的项目照旧,SDK 25 完全兼容)。 - 验证:一个 Spring Boot 4 + Java 25 的项目
mvn compile一次通过。某遗留模块编不过,查明是公司内部 BOM 不在公服镜像上(需先装父工程),与 JDK 无关——失败排查要先看 ERROR 首行归因,别急着怪环境。
最终版图:构建(maven)+ IDE 项目 + 终端全部 Oracle JDK25,IDE 自身跑 JBR(JetBrains 强制要求,动不得)。
第七步:JDK 本体也迁入 brew,IDE 只剩一个 SDK
第六步统一完”用哪个 JDK”,顺手把 JDK 本体也迁入 brew:brew install openjdk@25(25.0.4.1,比手动装的 Oracle 25.0.2 还新;keg-only,只上架不链接,不污染系统 java)。迁移成本比预期低很多:IDEA 里 SDK 名字保持 openjdk-25 不变,只改 jdk.table.xml 里一条 homePath(同路径替换 139 处),37 个项目的 misc.xml 一个都不用动;.zshrc 的 JAVA_HOME 同步换一行。
然后把旧 JDK 全清掉:brew JDK27(391MB)、微软 JDK21(336MB)、Oracle 25(370MB)、JBR 17/21(562MB),一轮约 1.6GB。删 JBR 前确认三件事:IDE 没在运行、没有自定义 boot JDK 文件(*.jdk)、没有项目引用——IDE 自带的 JBR 在各自 .app 包里,不受影响。
最终:/Library 与 ~/Library 的 JavaVirtualMachines 全空,全系统只剩 brew 一个 JDK;jdk.table 只剩 openjdk-25 一条注册。四个小 App(网易云/V2rayU/豆包/Tunnelblick)因 Gatekeeper 弹窗与官方下架问题保持手动安装,见踩坑 11。
踩坑总结
- AI 会话清理默认关闭:第一版把 Codex 30 天前的会话纳入自动清理,复核时决定太激进——会话记录可能是唯一的上下文回溯手段。改为
CLEAN_AI_SESSIONS开关,默认只报告占用(5.7GB),想清再手动开。删除类自动化的默认姿态应该是保守的。 brew cleanup -s删的不是”待安装文件”:有人担心刚下载还没装的包被清掉。实测验证:brew 缓存目录只有 brew 自己会写,浏览器下载的东西在~/Downloads;且被清的 4 个 Cask(微信/ChatGPT/WPS/Claude)既不在brew list里,也不在/Applications里,是纯孤儿残留。最坏情况也不过是重装时重新下载一次。MobileSync删不动是 TCC,不是权限:~/Library/Application Support/MobileSync/Backup里躺着 48GB 的 iPhone 本地备份,终端rm直接Operation not permitted——连 sudo 都绕不过。正确姿势是回「系统设置 → 通用 → 储存空间 → iOS文件」点删除按钮,苹果官方路径反而最干净。- 截图的临时文件会说话:排查时遇到
$TMPDIR/TemporaryItems/NSIRD_screencaptureui_xxx/下的截图暂存,点开一看正是储存空间管理里的 iOS 文件界面——顺藤摸瓜发现了上面那个 48GB 备份。临时目录(重启自动清空)本身不用管,但它的内容经常是绝佳线索。 pnpm找不到是因为 corepack:非登录 shell 里command -v pnpm为空,本机 pnpm 是 corepack 托管的。脚本里用command -v做存在性保护,找不到就跳过而不是报错——定时任务环境和交互终端的 PATH 差异,写脚本时就要预留。- 包名不等于可用性:
brew install codex成功不代表codex命令可用——装上的是 Cask,shim 指向的二进制根本不存在。铁律:装完必做which xxx && xxx --version双验证,有问题当场换渠道(这里是 brew 版 node 的 npm 全局)。 - nvm 孤儿化检查:
.zshrc里没有 nvm 加载行,~/.nvm就是 300 多 MB 的死目录。判断包管理器是否生效,不要看目录在不在,要看 shell 启动文件里有没有它的初始化——反之.zprofile里的brew shellenv和 OrbStack init 才是 PATH 重复的真凶,根治靠.zshrc末尾全局去重,而不是逐个加守卫。 - IDE 配置要趁它没运行时改:
jdk.table.xml和misc.xml都是 IDEA 启动时读、退出时写的,开着 IDEA 改会被覆盖。动手前pgrep确认,改完先备份。 - 编译失败先看 ERROR 首行归因:遗留模块编不过,第一反应可能是 JDK 版本问题,实际是内部 BOM 缺失。养成习惯:
grep ERROR看前 3 行定性(依赖缺失 / 网络 / 工具链),再决定动哪儿。 - cask 也有残次品和下架品:
brew install codex装上的 shim 指向不存在的二进制;v2rayu直接被官方下架(过不了 Gatekeeper)。brew 不是万能药,装完which + --version双验证,不行就换渠道。 - Gatekeeper 弹窗是非交互自动化的天敌:两个 App 的 dmg 在挂载时弹”磁盘映像可能有问题”,要图形界面点确认,脚本里直接 cancel——这类就留给人在自己终端里手动跑一次过掉,别跟自动化较劲。
- 校验脚本的 bug 也是 bug:一次复核把 JBR 标成”死链”,原因只是没展开
$USER_HOME$变量。教训:先确认断言本身是对的,误报比漏报更浪费信任。
DISCUSSION
讨论与反馈