• 问题: 把一台 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。

把每个进程自己报的数加起来所有进程 ri_phys_footprint 之和,对比机器自己说它用掉的内存。某天下午37.04 GB29.73 GB另一天29.81 GB26.94 GB各进程自报之和机器实际在用
两次测量里,加起来都比整台机器还多,分别多出 7.3 GB 和 2.9 GB。这不是出错了。三个进程共享的一页内存会被数三次,而 Mac 上共享的东西非常多。

这不是出错了。每个进程报的是它的 footprint,统计的是它碰到过的所有内存页。框架、共享库缓存、压缩页会被很多进程同时碰到,所以三个进程共享的一页会被数三次。Mac 上共享的东西非常多,开的 App 越多,这个和离现实越远。

一个 App 碰到多少,和它能还回来多少

关于一个 App 的内存,可以问它碰到了多少,也可以问它退出后能回来多少。前者就是 footprint。后者是它退出前后,机器自己报的"已用"差了多少。这个数更小,因为它和别的进程共享的那些页不会跟着走。

两个数单独看都没错,错在把它们放进同一句话里。菜单栏按 footprint 列出 App,再说能释放它们的总和,就是这么干的。在这台机器上的某一组 App 上,我们自己的菜单栏写着"可释放 10.66 GB",如实算只有 8.5 GB。

所以现在每个承诺内存的界面都改用第二个数。折算比例在全部 App 上算一次,不在某个界面碰巧列出的那一部分上算,否则同一台机器上的同一个 App,内存页面和菜单栏会给出两个答案。分组的表头也等于它下面那些行的和。这听起来是废话,可之前并不成立:一个 8 GB 的表头下面,几行加起来有 14 GB。

这个数永远不会被放大。 机器报出来的已用内存就是上限,归因只会把数往下调,朝它靠拢。

承诺和回执为什么对不上

单位统一之后,才能把界面承诺的和实际发生的放在一起比。我们把同一个 2.1 GB 的进程释放了三次,两个数都量了。

界面承诺的,和机器真正还回来的同一个 2.1 GB 进程的三次真实释放,单位 GB。承诺事后实测第一次1.732.15第二次1.731.39第三次1.871.99每一次,回执和随后重新读一遍机器的结果都相差 1% 到 3%:1.391 对 1.406,1.989 对 1.950。
这两个数本来就不该相等。承诺是按这台机器的平均共享程度,算出某个 App 该分到多少;回执是那个 App 真退出之后实际回来了多少。它们必须一致的是单位,不是数值。

两个方向都偏过,这很正常。承诺是按这台机器的平均共享程度,算出这个 App 该分到多少;回执是那个 App 退出之后实际回来了多少。私有内存占比高的 App 会超出承诺,共享得多的会达不到。单位统一之前,一个承诺甚至可能超过整台机器正在用的内存总量。把这两个数并排放出来,那种错就藏不住了。

必须对上的,是回执和紧接着重新读一遍机器的结果:1.391 对 1.406,1.989 对 1.950。回执上写的,就是你一秒后会看到的。

以前的"闲置"是怎么算的

另一半问题是哪些行会默认勾上。旧规则要求这个 App 占用较大、是普通应用、可以礼貌退出,并且闲置。闲置看的是它上一次在前台是什么时候。

哪些行会默认被勾选同样三行,在旧规则和替换它的新规则下的表现。旧规则新规则一个终端,你两小时没点过它这两小时里一直有 agent 在里面干活一个占了 2 GB 的浏览器,确实没在用没有任务在跑,也没托管别的程序一个占着 34% 单核的 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 会列出这些行,写明它们在干什么,但不勾选。你想退出,点一下就行;它不会因为你刚好没看那个窗口就被退出。

回头看这件事

算术一直没错。我们一开始拿到的两个数,测的是两件不同的事,而且都测对了,问题出在一句话把其中一个放到了另一个的位置上。承诺和回执用同一个单位以后,事后就能拿来对账,不用光听工具自己说。

"闲置"说的是一台机器在干什么,所以应该在这台机器上量。注意力是最好读的信号,但它回答的是另一个问题。看工作量的那几个信号多写了些代码,而它们用到的数据本来就在为别的功能采集。