一、满城秽物,无人认领

长安城出过一个怪毛病——满街堆着东西,却没人知道哪些还在用、哪些早该扔了。

起初只是零星的杂物。南市口扔着一只破竹筐,西坊墙根靠着一卷烂草席,东街槐树下歪着一辆缺了轮子的板车。没人管。日子一长,竹筐生虫,草席发霉,板车的木头朽进了土里。再后来,半座城的巷子被杂物堵死,行人侧着身子走,马车干脆过不去。

京兆尹急了,把城里资格最老的清道夫请了来。老魏干了三十年的街道清扫,头发胡子白完了,但一双眼睛亮得很。

"这些东西,"京兆尹指着满街杂物,"哪些还有主,哪些该清走?"

老魏四下看了看。"查一下就知道了。不过——查的时候,城里得停一会儿。"

二、从根上走一遍

老魏的法子是这样的。

他从皇城的正门出发。正门、朱雀大街、东西两市、各坊坊门——这些是长安城的"根"。根上拴着长安城还在运转的一切:衙门的文书、酒肆的桌椅、民宅的锅碗、学堂的笔墨。

老魏带了一队人手,每人手里攥着一支粉笔。从"根"开始,顺着每一条能走通的路往前走。大街通小巷,小巷通人家,人家通厢房,厢房通柜子。凡是能走到的物件,伸手摸一下,画一道粉笔印。

"粉笔记号是什么意思?"京兆尹问。

"有粉笔印的——有人用。"老魏说,"从城门走过来,能走着够着的,就说明这东西拴在某一条活路上。还没断。"

画完记号花了整整一个时辰。城里一切活动都停了——店铺关门,行人站住,马车歇在路边。长安城安静得像一张铺开的地图。

随后,老魏让另一队人手进场。这回的任务更简单:凡是没有粉笔印的东西,全部清走。

"这就对了。"京兆尹看着一条条被清空的巷子,"从前没人说得清哪些该留、哪些该清。因为没有一个人把整座城走通过。"

小帚是老魏新收的徒弟,他问:"师父,为什么必须从城门开始走?不能随便找个地方起头吗?"

"你要是不从城门走——从半路随便捡一件东西开始——你怎么知道这东西不是早该扔的?"老魏说,"根上有路一直通向它的,是活的。路上断了,或者根本走不到的——那就是早死了,只是尸体还没人收。"

三、市集死得快,老宅活得久

清完一次城,长安干净了。但不出一个月,杂物又堆起来了。

老魏不慌。他注意到一件事:南市的杂物永远最多,换得最快。那些菜叶子、破麻袋、断麻绳,昨天扔的今天就不要了,活不过三日。可老宅里的旧箱笼、老柜子、祖传的砚台,一放就是几十年。

"我们不能整座城每次都从头走一遍。"老魏对小帚说,"市集的东西死得快,我们就勤扫——每天清一次。老宅的东西活得久,一个月才去看一趟就行。"

小帚懂了。"市集天天去,老宅月底去。市集面积小,扫起来快;老宅虽然大,但几个月也没几样要扔的。"

"还有一桩。"老魏压低声音,"扫市集的时候,老宅的人不用停。扫老宅的时候,市集照常开。"

于是长安城的清扫分了代:年轻杂物进市集,每天一清;老东西进老宅,月底才动。 每次只清一小片区域,城里大部分地方该干嘛干嘛。

京兆尹算了一笔账:从前全城清扫一次,停一个时辰,怨声载道。如今拆成小块,每次只停一盏茶的工夫,还没人察觉到停顿,地已经干净了。

"这不是清得更快了,"小帚说,"是清得更聪明了——因为知道了什么东西死得早。"

四、城墙上的粉笔印

但老魏的规矩里有一条死命令——清扫时,被扫的那片区域必须停下来。

"为什么不能一边扫地一边让人走路?"

"你试试看。"老魏递了把扫帚给小帚,"南市开着的时候去扫地。"

小帚扛着扫帚去了。菜贩子扔了一个破筐,他刚要扫走,旁边伸来一只手——"筐我还要的,放那儿。"他往西走,一个小孩抱起地上的麻绳就跑。他追到东边,一脚踩进刚扫完的地,又脏了。

小帚回来,满脸通红。"根本扫不干净。人在动,东西在变——我刚标记的'垃圾',下一秒就被人捡走了。或者刚标记的'在用',下一秒就不要了。"

"这就是为什么得停下来。"老魏说,"我画粉笔记号的时候,城不能动。我要是边画边让人走来走去——粉笔印还没画完,刚才画过的地方路已经变了。粉笔印靠不住,清扫就全乱了。"

后来长安城立了规矩:每次清扫前,打更的敲三下梆子。沿街的人把脚边的要紧东西拢一拢——三声梆子过后,被扫的那片坊门暂时关闭。百来下心跳的工夫,清扫结束,坊门重开。

"不是不想让它一直开着。是算账的时候,得先把账本合上。"

五、扫帚下的长安

老魏老了,扫不动了。小帚接了他的位子。

新来的学徒问小帚:"师父,咱们的扫帚到底凭什么比别家厉害?"

小帚想了想,伸出三根手指。

"第一,从根上走。不从城门出发,你就不知道什么是活的、什么是死的。第二,年轻的死得快,天天扫一小块——别等满城都是垃圾了才动手。第三,扫的时候停下来算清楚——宁可停一小会儿,也不扫错一个。"

学徒望着长安城。南市人声鼎沸,老宅安静如常。上个月被清走的东西,早没人记得了。而那些从城门一路画着粉笔印连过来的——长安城的根、脉、命——依然在运转。

"一条街能走通,东西就活着。走不通的,再好也该扔了。"

技术解读

垃圾回收(Garbage Collection, GC)是编程语言运行时自动管理内存的核心机制。程序员分配内存后不需要手动释放——GC 会在运行时自动识别并回收不再被引用的对象。最早的垃圾回收可追溯到 1959 年 John McCarthy 为 Lisp 语言设计的标记-清除算法。

现代 GC 的核心思想是可达性分析:从一组根对象(堆栈、全局变量、寄存器中的引用)出发,沿着引用链遍历所有可到达的对象——被遍历到的对象是"活的",其余的都是"垃圾",可以被回收。

标记-清除(Mark-and-Sweep)是最基础的 GC 算法:标记阶段从根出发遍历并标记所有存活对象;清除阶段遍历整个堆,将未标记的对象的内存释放。但标记-清除有两个问题:一是需要扫描整个堆导致停顿时间长;二是"年轻对象死得快"这一经验规律没有被利用。

分代回收(Generational GC)基于弱分代假说(weak generational hypothesis):绝大多数对象都是朝生夕死的。将堆分为年轻代(young generation)和老年代(old generation),年轻代使用频繁但快速的小型 GC(minor GC),老年代在年轻代多次回收后仍存活的对象被晋升(promotion),仅在必要时执行全堆回收(major/full GC)。复制算法常用于年轻代,标记-清除或标记-整理常用于老年代。

"停止-世界"(Stop-the-World)是 GC 的最大痛点——回收期间应用线程必须暂停以确保对象图的一致性。现代 GC 通过并发标记、增量回收、读写屏障等技术逐步缩短停顿时间。

核心概念回顾

概念 通俗解释
根对象(Roots) GC 遍历的起点——栈上的变量、全局变量、寄存器中的引用
可达性分析 从根出发走一遍引用链,走到的是活的,走不到的是垃圾
标记-清除(Mark-Sweep) 先标记所有活着的东西,再把没标记的全清掉
弱分代假说 大多数对象创建后很快就没用了,活得越久越可能继续活
年轻代 / 老年代 刚创建的对象放年轻代,经历多次 GC 还活着的晋升到老年代
Minor GC / Major GC 年轻代的快速回收 / 全堆回收(包括老年代)
晋升(Promotion) 年轻代中经过若干次 GC 仍存活的对象被移到老年代
Stop-the-World GC 回收时必须暂停应用,防止对象图在回收过程中被修改
并发 GC GC 的标记工作可以与应用程序并发执行,减少停顿
写屏障(Write Barrier) 在并发标记期间,记录对象的引用变更,防止漏标

故事中的隐喻对照

故事元素 映射的技术概念 解释
长安城 进程的堆内存 所有动态分配的对象都在这座"城"里
城门 / 朱雀大街 / 坊门 根对象(GC Roots) GC 从此出发遍历可达对象——栈、全局变量、寄存器
粉笔标记 标记阶段 从根出发标记所有可达对象,确认它们是"活的"
能走通的路 引用链 如果存在从根到某对象的引用链,该对象可达
路断了、走不到 不可达对象 没有任何根能到达的对象就是垃圾
清扫没有粉笔印的 清除阶段(Sweep) 释放所有未被标记的对象
南市(年轻杂物死得快) 年轻代(Young Gen) 大多数对象在创建后很快变成垃圾
老宅(旧物不常扔) 老年代(Old Gen) 活得久的对象倾向于继续存活
市集天天扫、老宅月底去 Minor GC 频繁、Major GC 低频 年轻代回收频率高但快,老年代回收慢但次数少
三声梆子、坊门关闭 Stop-the-World 停顿 GC 时必须暂停应用线程以保证对象图一致性
边扫边让人走就乱了 并发修改导致漏标/错标 应用在 GC 期间修改引用会导致对象图的不一致
在扫的片区停,其他地方照常 分代 GC 只停年轻代 Minor GC 只暂停较短时间,老年代不受影响

为什么这个故事对应垃圾回收?

  1. 可达性分析是 GC 的灵魂:故事中"从城门出发,顺着每一条能走通的路,能走到的就画粉笔印"——这精确映射了 GC 从根对象出发进行可达性遍历的核心算法。粉笔印 = 标记位,走不通的路 = 断开的引用链。

  2. 分代假说驱动性能优化:南市垃圾三天就换(年轻代朝生夕死)而老宅百年不动(老年代稳定持久)——这是弱分代假说的完美隐喻。正是因为观察到了这个规律,分代 GC 才比全堆 GC 高效得多。

  3. 小区域高频次 + 大区域低频次:市集天天扫一次(Minor GC 频繁但快)、老宅月底去一次(Major GC 低频但慢)——现代 GC 的核心策略被压缩进了这个清洁制度里。

  4. Stop-the-World 的必要性:小帚尝试"边扫边让人走"而失败——这解释了为什么并发 GC 需要写屏障等复杂机制。在没有任何并发保护的情况下,"标记的时候城不能动"就是最简单也最安全的方案。

  5. "宁可停一小会儿,也不扫错一个"的工程取舍:GC 设计的永恒张力在于停顿时间 vs 吞吐量。短停顿拼的是频率快、范围小(年轻代),而不是妄想全无停顿。

  6. 晋升机制的隐含映射:南市的杂物如果在多次清扫后还没被扔掉(说明有人在持续使用它),就应该搬到老宅去——这对应了年轻代对象经过若干次 Minor GC 后晋升到老年代。

后记:垃圾回收的哲学隐喻远比计算机科学更古老——任何有生命的系统都需要区分"还在用的"和"已经死去的"。一座不清理的城市会被废弃的杂物窒息,正如一个不回收内存的程序会被死对象撑爆。老魏扫了三十年街,真正学会的不是怎么扫地,而是怎么辨认死活——从根上走,走得通的留,走不通的弃。那一支粉笔看似轻巧,画出的却是整座城市的命脉。