起因

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/downloads6.1GBbrew 下载缓存
~/.npm/_cacache4.8GBnpm 缓存
~/.codex/sessions5.7GBCodex 历史会话
~/Library/Caches/JetBrains/IntelliJIdea2026.13.4GBIDE 旧版本缓存(当前 2026.2)
~/Library/Caches/ms-playwright1.1GB旧浏览器内核
~/Library/Logs/JetBrains764MBIDE 日志

需手动确认(约 14GB):

位置大小备注
~/VirtualBox VMs/basic_bak5.4GB与 basic 重复的整机备份
~/Downloads/ubuntu-*.iso2.8GB用完即删的典型
~/Library/pnpm/store5.0GB可删一次,后续 prune 维持
~/Documents/某业务流程图.zip408MB已有同名解压目录,纯重复

碰都别碰: ~/.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 步,每一步都是”条件触发 + 可再生”:

  1. Homebrew 缓存 → brew cleanup -s + autoremove
  2. npm/pnpm 缓存 → npm 超 1GB 才清,pnpm 只 store prune
  3. JetBrains 旧版 → 自动识别版本号最大的保留,其余删除
  4. Codex/Claude 会话 → 默认关闭,CLEAN_AI_SESSIONS=1 才删(见踩坑 1)
  5. Maven/Gradle → 只删 *.lastUpdated 失败标记,不动仓库主体
  6. 通用日志 → *.log 按 14/30 天滚动
  7. 回收站/会话 → 只清 30 天前的
  8. 项目构建产物 → 默认关闭,CLEAN_BUILD_OUTPUTS=1 才删 target/build/dist
  9. 大户报告 → 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,这样周一的自动更新流水线顺手就管了:

工具之前之后
codexnvm 里的 symlink 目标失踪,全终端不可用brew 版 node 的 npm 全局重装,0.155.1 ✅
opencodepnpm 版已坏 + 官方版两处重复都不要了,三处全删
node 版本管理孤儿 nvm(.zshrc 无加载行)+ brew node@24删 nvm,改用 brew 多版本:node@24(主)+ node@22(LTS),brew unlink/link 切换
gitApple Git 2.54.0brew Git 2.55.0(配置与凭证助手通用,无缝)
新增—gh(GitHub CLI)
python3.13 + 3.14 并存删 3.13(无反向依赖),留 3.14
openjdk27brew 版(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。

踩坑总结

  1. AI 会话清理默认关闭:第一版把 Codex 30 天前的会话纳入自动清理,复核时决定太激进——会话记录可能是唯一的上下文回溯手段。改为 CLEAN_AI_SESSIONS 开关,默认只报告占用(5.7GB),想清再手动开。删除类自动化的默认姿态应该是保守的。
  2. brew cleanup -s 删的不是”待安装文件”:有人担心刚下载还没装的包被清掉。实测验证:brew 缓存目录只有 brew 自己会写,浏览器下载的东西在 ~/Downloads;且被清的 4 个 Cask(微信/ChatGPT/WPS/Claude)既不在 brew list 里,也不在 /Applications 里,是纯孤儿残留。最坏情况也不过是重装时重新下载一次。
  3. MobileSync 删不动是 TCC,不是权限:~/Library/Application Support/MobileSync/Backup 里躺着 48GB 的 iPhone 本地备份,终端 rm 直接 Operation not permitted——连 sudo 都绕不过。正确姿势是回「系统设置 → 通用 → 储存空间 → iOS文件」点删除按钮,苹果官方路径反而最干净。
  4. 截图的临时文件会说话:排查时遇到 $TMPDIR/TemporaryItems/NSIRD_screencaptureui_xxx/ 下的截图暂存,点开一看正是储存空间管理里的 iOS 文件界面——顺藤摸瓜发现了上面那个 48GB 备份。临时目录(重启自动清空)本身不用管,但它的内容经常是绝佳线索。
  5. pnpm 找不到是因为 corepack:非登录 shell 里 command -v pnpm 为空,本机 pnpm 是 corepack 托管的。脚本里用 command -v 做存在性保护,找不到就跳过而不是报错——定时任务环境和交互终端的 PATH 差异,写脚本时就要预留。
  6. 包名不等于可用性:brew install codex 成功不代表 codex 命令可用——装上的是 Cask,shim 指向的二进制根本不存在。铁律:装完必做 which xxx && xxx --version 双验证,有问题当场换渠道(这里是 brew 版 node 的 npm 全局)。
  7. nvm 孤儿化检查:.zshrc 里没有 nvm 加载行,~/.nvm 就是 300 多 MB 的死目录。判断包管理器是否生效,不要看目录在不在,要看 shell 启动文件里有没有它的初始化——反之 .zprofile 里的 brew shellenv 和 OrbStack init 才是 PATH 重复的真凶,根治靠 .zshrc 末尾全局去重,而不是逐个加守卫。
  8. IDE 配置要趁它没运行时改:jdk.table.xml 和 misc.xml 都是 IDEA 启动时读、退出时写的,开着 IDEA 改会被覆盖。动手前 pgrep 确认,改完先备份。
  9. 编译失败先看 ERROR 首行归因:遗留模块编不过,第一反应可能是 JDK 版本问题,实际是内部 BOM 缺失。养成习惯:grep ERROR 看前 3 行定性(依赖缺失 / 网络 / 工具链),再决定动哪儿。
  10. cask 也有残次品和下架品:brew install codex 装上的 shim 指向不存在的二进制;v2rayu 直接被官方下架(过不了 Gatekeeper)。brew 不是万能药,装完 which + --version 双验证,不行就换渠道。
  11. Gatekeeper 弹窗是非交互自动化的天敌:两个 App 的 dmg 在挂载时弹”磁盘映像可能有问题”,要图形界面点确认,脚本里直接 cancel——这类就留给人在自己终端里手动跑一次过掉,别跟自动化较劲。
  12. 校验脚本的 bug 也是 bug:一次复核把 JBR 标成”死链”,原因只是没展开 $USER_HOME$ 变量。教训:先确认断言本身是对的,误报比漏报更浪费信任。