垃圾回收是一個錯誤
蘋果最近寫了一篇很輕描淡寫的技術(shù)遷移文:他們把密碼管理服務(wù)從 Java 搬到了 Swift,性能一飛沖天。
寫得很克制,結(jié)果信息量爆炸。
看完只想說一句:原來不是我們寫得不行,是垃圾回收(GC)這條路本身就有坑。
蘋果點破了那個所有人都不想說的真相
文里有一段話反復(fù)看了三遍,給你摘出來:
我們先沒急著換語言,先是把 JVM 能調(diào)的參數(shù)都調(diào)了。 用了 G1 GC,這個 GC 比老一代強很多:能控制暫停時間、有分區(qū)、還能并行干活。 ——但!在高并發(fā)場景下,GC 還是難搞:一來會有長暫停,二來性能開銷會頂上來,三來要想讓它適配各種各樣的業(yè)務(wù),就得一直細(xì)調(diào)、細(xì)調(diào)、再細(xì)調(diào)。
看到?jīng)]?這已經(jīng)不是“我寫了個大對象結(jié)果GC卡了一下”這種程度的問題了,這是蘋果這種量級的公司都壓不住的系統(tǒng)性問題。
你要知道,他們干的還是“密碼管理”這種活——高頻、敏感、延遲敏感、請求量巨大的線上服務(wù)。
“討厭GC”其實很多人想說但不敢說
大家對 GC 的情緒也不算中立的。
在移動端上這一點更明顯: 你認(rèn)真滾動一個 Flutter 界面,動畫正絲滑,GC 突然說:該洗一洗內(nèi)存了——咔!掉幀。
你用戶根本不在乎你在做什么回收算法,只知道:“怎么卡了一下?”
以前總覺得: “行,客戶端容易卡是因為 UI 線程比較嬌貴;但在服務(wù)器上,總有 20ms 的網(wǎng)絡(luò)延遲,GC 卡個十幾毫秒,應(yīng)該沒人能看出來吧?”
——蘋果這篇文直接告訴你:想多了。
它不是“偶然卡一下”的問題,是你量一旦上來了,GC 這套機制本身就變成了阻力。 一個密碼管理服務(wù),全球幾百萬、幾千萬設(shè)備,分分鐘都是對象分配和釋放,GC 的成本就不再是“小數(shù)點后第二位”的事了。
先回憶一下:GC 最原始的樣子有多粗暴
一開始的垃圾回收很樸素:停世界,涂顏色。
- 程序停一停,誰也別動
 - 從“根”(棧、全局、寄存器)開始,把能走到的對象都標(biāo)成“活的”
 - 其他的都當(dāng)垃圾,一把丟掉
 
這種叫標(biāo)記-清除,思想簡單,工程實現(xiàn)也比較直觀。 問題也很直觀:你得停。
游戲里就是卡頓,前端里就是掉幀,后端里就是尾延遲躥上去。
于是這一停世界,就讓幾十年的語言設(shè)計師、JVM 工程師、runtime 大神們開始拼命“降卡頓”。
然后我們走上了一條“工程師自我感動”的路
于是就有了各種“聽起來很高級”的GC:
- 分代回收
 - 并行、并發(fā)回收
 - 分區(qū) / region-based(Java 的 G1 就是這個)
 - 可預(yù)測暫停時間
 - 增量式、分片式、后臺標(biāo)記……
 
聽上去是不是很厲害?
但蘋果那句話其實已經(jīng)說透了:它們都沒解決根問題,只是把大坑切成了很多小坑。
GC 還是得暫停的,只是從“一次停很久”變成了“停很多次、每次短一點”。 而且更要命的是:你分區(qū)、你分代,其實都是在給 runtime 自己加開銷。在高負(fù)載場景下,這個開銷很快就會浮出來。
所以蘋果最后說:我們還是換語言吧。
你以為蘋果會秒選 Swift?他們也猶豫過
他們自己寫的:我們其實看了好多語言,不是那種“蘋果出品必用 Swift”那種拍腦袋。
最后還是落到了 Swift,核心原因其實就一個:它不用垃圾回收。
Swift 用的是 ARC(Automatic Reference Counting,自動引用計數(shù))。
它不搞“我過一陣子再來看看誰還能活著”這一套, 它搞的是:“你被引用+1,你不被引用-1;你變成0,我立刻干掉你?!?/span>
這個思路有兩個驚人的優(yōu)點:
- 釋放是分散的,不是集中的所以不會突然來一刀,整個線程都進去回收
 - 時機更可預(yù)期引用一旦沒了,馬上釋放,不會拖到幾秒鐘后再清
 
這,就是為什么在 UI 場景下,ARC 體驗肉眼可見地更順。
蘋果現(xiàn)在的說法是:同樣的爽感,在服務(wù)器上也能拿到。
當(dāng)然,ARC 也不是天使
你要是只說“GC 有暫停,ARC 沒暫?!?,那就太不講究了。
ARC 最大的雷,大家都知道:循環(huán)引用。
A 引用 B,B 引用 A,兩個都有人愛,引用計數(shù)永遠不會變成 0, 但其實外面沒人能走到它們了——這就是一坨泄露。
Swift 的做法是: 給你弱引用(weak)、unowned 這種不參與計數(shù)的引用, 你自己在該用的地方用,不該用的地方別用。
也就是說:ARC 不是傻瓜式的。你還是得懂點內(nèi)存生命周期,不然你也能寫出一鍋爛。
不過話說回來,GC 也不是傻瓜式的。
你要是一直往一個全局 List 里塞對象、從來不刪,GC 也清不了你。 本質(zhì)上還是:語言幫你減了 80% 的內(nèi)存活,剩下 20% 還是你自己得有腦子。
所以問題就變成了: ?? 為了這 20%,你愿不愿意換來一個“幾乎不掉幀、幾乎不突刺、尾延遲更穩(wěn)”的世界?
蘋果的答案是:愿意。
那么問題來了:既然 ARC、所有權(quán)、arena 這些都在,為什么當(dāng)年我們選了 GC?
這才是本文真正的靈魂拷問。
你看可選方案其實一大堆:
- 像 Swift 這樣:引用計數(shù) + 弱引用
 - 像 Rust 這樣:所有權(quán) + 借用檢查
 - 像 Ada 那樣:只能指向“更深”的對象,天生不容易環(huán)
 - 像游戲引擎常用的:區(qū)域 / arena 分配,一幀分配完,一幀回收
 - 像 Jai、很多高性能系統(tǒng)用的:一大塊內(nèi)存開出來,用完一刀清
 - 甚至還有引用計數(shù) + 少量GC兜底這種折衷
 
你看,路線這么多,我們偏偏走了“讓程序自己去猜你要不要內(nèi)存”的那條。
這事你要是放到公司里來,是不是像這樣
甲:我們能不能在寫代碼的時候就標(biāo)一下“我這個對象多久用完”? 乙:不用,我們寫個超級復(fù)雜的系統(tǒng),隔一會就去全內(nèi)存里掃一遍,看有沒有人不要了。 甲:???
如果這是你同事提的方案,你是不是要拍桌子了?
可是現(xiàn)實世界里,我們真的就這么干了,而且還干了30年。
為啥?我也覺得就是營銷贏了
“看,我們這是像 C 一樣快的語言,但你不用自己 free?!?“你再也不用擔(dān)心內(nèi)存泄露了!” “寫業(yè)務(wù)就行,內(nèi)存我們管?!?/span>
很香,對吧。
于是 Java 火了,.NET 火了,很多企業(yè)都投了,連蘋果這種公司都在用它的 JVM。
但代價是什么?
一整代工程師被教育成:不需要顯式表達生命周期。
然后 runtime 就只能跟個猜你心思的戀人一樣,一遍一遍全內(nèi)存掃,想看你到底還愛不愛這個對象。
每一次掃,都是CPU、內(nèi)存、能源的浪費。
我看到一個 HN 的評論,真是一針見血,我給你翻一下
垃圾回收就是“沉沒成本”的典范。我們30年都在往一個壞主意上砸聰明人。 真相其實是:我們不愿意在語言層面、語法層面給足編譯器“對象會活多久”的上下文; 于是就只好搞一個近乎自我感知的系統(tǒng),去猜我們是不是用完了。 它浪費時間、浪費空間、浪費能量。 要是當(dāng)年這些人去做“更聰明的語言”,而不是去搞“更聰明的GC”,我們今天會過得好很多。 GC 就是壞主意。我高興看到我們終于準(zhǔn)備好往前走了。
你看,這個吐槽有多狠。我們不是不會表達生命周期,是我們懶得表達。于是就有了一個“全內(nèi)存 for 一遍”的東西。
這也是為什么你越調(diào) G1,越覺得像是在貼創(chuàng)可貼
G1、ZGC、Shenandoah…… 名字一個比一個酷, 實現(xiàn)一個比一個復(fù)雜, 論文一個比一個厚, ——但本質(zhì)還是:“我還是要掃,只是我盡量別嚇到你?!?/span>
從結(jié)果看,蘋果還是被嚇到了。 Flutter 開發(fā)者也會被嚇到。 高頻客戶端會被嚇到。 高并發(fā)后端也會被嚇到。
說明問題不在于我們調(diào)得還不夠精妙。是我們走的是不該走的那條路。
如果我來設(shè)計語言,我會這么干
作者原文里說了一句我還挺認(rèn)同的思路,大意是:
我會做一個類似 ARC 的系統(tǒng),但我只允許一個“硬”引用指向某個堆對象,這樣循環(huán)引用從語義上就不成立了,基本上就能做到“又快又傻瓜”。
這其實跟 Rust 的“所有權(quán)只能有一個”有點像,只是思路沒那么硬。 核心都是那個意思:別再讓 runtime 替我們猜了,我們自己說。
而且就算你不喜歡這種路線,也不是非GC不可。 你上面看到了,方法一大堆。
所以,GC 是不是錯了?
如果你的目標(biāo)是
- 開發(fā)體驗第一
 - 要在 2000 年初對比 C++ 看起來“好輕松哦”
 - 要讓一大批企業(yè)開發(fā)者迅速寫業(yè)務(wù)
 - 要讓培訓(xùn)班能一周教完內(nèi)存
 - 要發(fā)個“write once, run anywhere”式的廣告
 
那 GC 沒錯,它甚至是商業(yè)上最正確的選擇。
但如果你的目標(biāo)是??
- 幀率不能抖
 - 后端尾延遲不能飄
 - 能源要省
 - 云成本要控
 - 語言要能清晰表達生命周期
 - 工程要能被未來的人維護
 
那對不起,GC 這條線,確實該被重新審視了。
蘋果這次遷移,其實就是很大聲地說了這一句:“我們在 JVM 上已經(jīng)把能調(diào)的都調(diào)了,但GC本身還是我們的天花板?!?/span>
這句話,才是最有含金量的。
最后一口狠話
你這么想一下就很魔幻:
我們沒有想辦法在語言里好好說“我這個對象什么時候不用了”; 結(jié)果我們寫了一個巨大的、常駐的、會自己醒來的、會遍歷整個進程內(nèi)存的大型循環(huán), 去猜我們到底用不用一個對象。
要是你公司里有人提說: “這事我不知道你什么時候不用,我就隔十秒全庫掃一遍好了?!?你是不是想說:你是不是有???
但我們對 GC 就是這個態(tài)度:
它已經(jīng)投了三十年了,不能說它錯。
能說。 蘋果已經(jīng)說了。
所以,這篇文章才會叫:Garbage Collection Was A Mistake。
不是說“所有帶GC的語言都不能用”——別搞極端。
而是說:我們這幾十年可能繞了個遠路,把問題丟給了runtime,而不是丟給語言設(shè)計。
現(xiàn)在好了,Rust 出來了,Swift ARC 跑起來了,游戲引擎的 arena 模式走得飛快,WebAssembly 的內(nèi)存故事也開始講了,
我們終于有勇氣回頭看一眼:當(dāng)年那個“全內(nèi)存掃一遍”的主意,是不是有點太粗了。















 
 
 









 
 
 
 