- 问题: 把一台 Mac 上每个进程报的内存加起来是 37.04 GB,机器自己却说只用了 29.73 GB。几个 App 共享的内存,被每个 App 各算了一次。
- 我们发现: 退出一个 App 拿回的内存,比列表里写的少。对同一组 App,我们自己的菜单栏写着可释放 10.66 GB,实际只有 8.5 GB。
- 我们做了什么: Mint 里所有承诺能腾出内存的地方,现在都按退出后真正能拿回的量来算。背后还在干活的行,比如正在跑 Claude Code 的终端,会列出来但不默认勾选。
为了腾内存而退出 App 之前,先看清数字衡量的是什么。把每个 App 报的占用相加,会重复计算共享内存,这个总数并不等于退出后能拿回的空间。我们在一台 Mac 上测了两者的差别,也检查了为什么正在后台跑编程 Agent 的终端不能被当作闲置 App。
如果 Mac 正在变卡、Agent 还在工作,可以先看AI 任务运行时的内存检查指南:先判断内存压力,再检查哪些应用可以考虑退出,最后确认实际变化。
问 Mac 上的每一个进程"你用了多少内存",把答案加起来,会得到一个比整台机器还大的数。某天下午这个和是 37.04 GB,而机器自己报告的已用内存是 29.73 GB。换一天再测一次:29.81 对 26.94。
这不是出错了。每个进程报的是它的 footprint,统计的是它碰到过的所有内存页。框架、共享库缓存、压缩页会被很多进程同时碰到,所以三个进程共享的一页会被数三次。Mac 上共享的东西非常多,开的 App 越多,这个和离现实越远。
一个 App 碰到多少,和它能还回来多少
关于一个 App 的内存,可以问它碰到了多少,也可以问它退出后能回来多少。前者就是 footprint。后者是它退出前后,机器自己报的"已用"差了多少。这个数更小,因为它和别的进程共享的那些页不会跟着走。
两个数单独看都没错,错在把它们放进同一句话里。菜单栏按 footprint 列出 App,再说能释放它们的总和,就是这么干的。在这台机器上的某一组 App 上,我们自己的菜单栏写着"可释放 10.66 GB",如实算只有 8.5 GB。
所以现在每个承诺内存的界面都改用第二个数。折算比例在全部 App 上算一次,不在某个界面碰巧列出的那一部分上算,否则同一台机器上的同一个 App,内存页面和菜单栏会给出两个答案。分组的表头也等于它下面那些行的和。这听起来是废话,可之前并不成立:一个 8 GB 的表头下面,几行加起来有 14 GB。
这个数永远不会被放大。 机器报出来的已用内存就是上限,归因只会把数往下调,朝它靠拢。
承诺和回执为什么对不上
单位统一之后,才能把界面承诺的和实际发生的放在一起比。我们把同一个 2.1 GB 的进程释放了三次,两个数都量了。
两个方向都偏过,这很正常。承诺是按这台机器的平均共享程度,算出这个 App 该分到多少;回执是那个 App 退出之后实际回来了多少。私有内存占比高的 App 会超出承诺,共享得多的会达不到。单位统一之前,一个承诺甚至可能超过整台机器正在用的内存总量。把这两个数并排放出来,那种错就藏不住了。
必须对上的,是回执和紧接着重新读一遍机器的结果:1.391 对 1.406,1.989 对 1.950。回执上写的,就是你一秒后会看到的。
以前的"闲置"是怎么算的
另一半问题是哪些行会默认勾上。旧规则要求这个 App 占用较大、是普通应用、可以礼貌退出,并且闲置。闲置看的是它上一次在前台是什么时候。
于是,一个两小时没被点过、但这两小时里一直有 agent 在里面干活的终端,被判成了整台机器上最闲的那个。Claude Desktop 里托管的会话、编辑器启动的 agent 工作进程,也这样被默认勾上了。Apple 自己的 App 因为别的原因从来不是候选,所以系统终端是侥幸躲过的,第三方终端都没有这层保护。
最省事的改法是什么都不默认勾选,我们没这么做。一个你没在用、却占着 2 GB 的浏览器,正是这个功能要处理的。要改的是规则问的那个问题。
新规则改问它有没有在干活,用的都是本来就在采集的信号。先看这一行,或者它启动的进程里,有没有托管的 agent。再看这一行连同下面所有进程,在采样窗口里有没有 CPU 活动。最后看这个 App 里有没有跑在 shell 下的程序,从外面看,终端和编辑器的内置终端就是这个样子。
有活在跑的行照样列出来,归进"使用中"那一堆,原因写在行上,只是不勾选。上面会写着"正在运行 Claude Code"或者"忙碌 · 34% CPU",等你来决定。
CPU 这部分花了我们一个下午。进程的 CPU 时间以 Mach tick 为单位返回,在 Apple Silicon 上换算不是一比一:一段我们计时为 125.7 毫秒的忙循环,报回来是 3,016,253 个 tick。这个比例得向系统要,不能靠在一台机器上看着对的算术。
我们做错的一件事,以及它是怎么自己现形的
我们的演示数据里,有一个后台辅助进程,进程号写的是 0。
释放路径先确认这个进程还活着,然后向它发了终止信号。在 Unix 上,发给进程 0 的信号会送到调用方的整个进程组。Mint 在 0.66 秒内把自己杀了,启动它的那个 shell 脚本也跟着一起退出。这是在一个专门用来做这类探测的副本上测到的,没有牵涉任何人的真实会话。
修复只有三行,想清楚它该放在哪里花的时间更久。从演示数据里来的数字,执行器同样会照着动手,所以检查要设在动手的地方,而不是写演示数据的地方。现在一行如果报出小于等于零的进程号,会被当作"已变化"直接丢弃,发信号的循环会跳过它,那里也有测试守着。演示数据里那个 0 特意保留着,以后每一个副本都会撞上这道防线。
从外面看,agent 像是闲着
上面图里的终端,就是开发者机器的日常,而且这样的机器每个月都在变多。一个 agent 会话,可能是终端里 shell 下的一个程序,可能是编辑器启动的一个辅助进程,也可能是一个托管着编码会话的桌面 App。从外面看,它们都像"一个很久没人点过的 App"。
内存工具得先把这种情况做对,你才敢让它一直开着,所以这条规则看的是有没有在干活,而不是你有没有在看它。Mint 会列出这些行,写明它们在干什么,但不勾选。你想退出,点一下就行;它不会因为你刚好没看那个窗口就被退出。
回头看这件事
算术一直没错。我们一开始拿到的两个数,测的是两件不同的事,而且都测对了,问题出在一句话把其中一个放到了另一个的位置上。承诺和回执用同一个单位以后,事后就能拿来对账,不用光听工具自己说。
"闲置"说的是一台机器在干什么,所以应该在这台机器上量。注意力是最好读的信号,但它回答的是另一个问题。看工作量的那几个信号多写了些代码,而它们用到的数据本来就在为别的功能采集。