記憶體尋夢環遊記:一個變數的三重死亡

doodlewind發表於2019-03-02

記憶體的世界

小 u 身高 64 位,是記憶體世界 number 家族裡的一名浮點數變數。因為小 u 身體的二進位制第一位是 0,所以按照 IEEE 754 標準,大家都把她當做女孩子來看待。她第 2 位到第 11 位的階碼並不夠大,使得她看起來小巧玲瓏;而她剩下的 52 個小數位十分精緻,這樣工作的時候和她打交道的變數舍入誤差都很小,所以大家都很喜歡她。

小 u 每天的工作,是在記憶體世界裡和其他的變數打交道,計算出有用的結果去造福人類世界。平時,在函式呼叫結束以後,小 u 就可以下班回到她在原始碼裡的家了。她的工作壓力不大,不像那些身處 for 迴圈里名叫 i 呀 j 呀的變數那樣需要不停地加班連軸轉。而她的家也是自她出生以來就由人類世界裡的程式設計師編寫好的。別看那些程式設計師穿著邋遢,但對原始碼卻像對待自己的孩子一樣寵愛。小 u 在原始碼裡的家就是用一種名叫 JavaScript 的材料建起來的,不光有五顏六色的編輯器主題來裝飾,還有嚴謹的分號和括號來保證家裡的結構的穩定和對稱,讓她很有安全感。

雖然有著可愛的外表、輕鬆的工作和舒心的家,但小 u 卻還是有著自己的煩惱:她的家族出身決定了她不能有伴侶。

在 JavaScript 這種材料所在的國度裡,number 家族隸屬於古老的基本型別家族。除了 number 之外,那些經典的資料結構,像字串 string 和空值 null,都屬於基本型別家族。由於簡單的基本型別很容易在程式碼裡被直譯器推斷出來,所以他們的記憶體都是在一種死板的『棧』空間上預先分配好而不可變的。哪怕是和其他 number 耳鬢廝磨地加加減減,也不能真正地在一起。

而與基本型別家族相對的,則是時髦的引用型別家族。那些人類程式設計師青睞的所謂『物件導向程式設計』,說的就是這個家族。這個家族的成員複雜而多變,因此他們會被分配到廣袤的『堆』空間上,相互之間經常是你中有我,我中有你的狀態。比起註定孤獨一生的基本型別家族,有物件的引用型別家族無疑要滋潤得多。

小 u 有個不敢說出口的夢想,那就是努力成為引用型別裡的一員。聽說在遠方的 Java 國度,有一條叫做『自動裝箱』的法律能夠讓自己的家族看起來像引用型別家族一樣,那樣她也許就可以不再孤獨了。

夢想歸夢想,她對自己的生活其實還是挺滿意的。在記憶體世界習慣之後,工作和生活的平衡是許多人類世界的程式設計師一輩子都達不到的。這樣的生活一直繼續著,直到有一天……

閉包的詛咒

那天像往常一樣,小 u 從原始碼的家裡出發,通過詞法分析門後,搭上了語法分析班車的軌道。班車上 JIT 的標識代表著 Just-In-Time,就好像人類世界中『JR 新幹線』和『和諧號』那樣,是高效、快捷的象徵。

班車迅速地把小 u 載到了語法樹軌道上的葉子節點站臺。走下班車,站臺上有一張 64 位尺寸的長椅。她坐上椅子閉上眼,等待著直譯器對她的掃描和呼叫。

『但願這次不要遇上粗俗的 null 值……』小 u 默唸著,眼前一陣電光閃動,隨著記憶體世界底層無數電晶體狀態的改變,直譯器如期讀取了小 u 的值。在這條原子性的指令裡,小 u 需要讓直譯器完全地控制自己,她從來不知道從電光閃動到再一次睜開眼睛之間,記憶體世界裡發生了什麼。

『嗯……』她如期醒來了,照理說她在醒來時還是會身處同樣的站臺位置,等待回程的語法樹班車接她回家。

眼前還是同樣的景象,不對,好像又有哪裡不一樣——站臺的結構和佈置似乎和之前別無二致,只是少了一樣東西:軌道上空空蕩蕩,沒有等待她的班車,更沒有別人。難道……誤點了?她打心裡不相信這樣低階的錯誤會出現頻率精準的記憶體世界裡。不過班車沒來就是沒來,她只好在站臺上繼續等待。

時間一赫茲一赫茲地經過,小 u 內心的不安和焦慮也在慢慢增加:到底發生了什麼?班車是忘記我了嗎?還是說提前開走了?女孩子一個人在外呆這麼久是很不安全的,但是作為嚴謹的變數,獨自行動更是記憶體世界裡的大忌。『還是……再等等吧……』小 u 有些絕望地想。

班車還是沒有到。

『不行了,我必須回原始碼裡去啊!』等待終於讓小 u 的情緒激動起來了,她開始在站臺上尋找其它的出口,想要找到回家的路。軌道不能跳下去,但站臺的兩頭有個紅色的 Exit 標識,那裡看起來是個可以通行的出口。不過現代程式語言國度裡的變數一般從來都不這麼走,因為手動的記憶體操作很危險。

小 u 打量四周,小心翼翼地推開了回程那頭 Exit 下鏽跡斑斑的門。謝天謝地,這裡是有路的,並且看起來不是那麼危險。她走過一段狹長的走道,走道里每隔固定的長度就會亮著一個小小的指示燈,看起來是記憶體地址空間的下標標識。終於,她看到了出口:一扇形狀相同的 Exit 門。小 u 迫不及待地推開門,想看看自己有沒有更接近家一點。

眼前的景象讓她詫異:一模一樣的軌道、一模一樣的長椅、一模一樣的站臺、一模一樣的 Exit,就好像自己根本沒有移動過一樣!

難道我走錯路了嗎?這不可能呀!小 u 對方向這樣非 0 即 1 的狀態有著絕對的自信,她知道她不會走錯的。也許這段地址空間裡的內容都是這樣吧?沒事的,再走走就不一樣了吧。於是,天真的她開始了漫長的步行,然而讓她一點點喪失信心的是,每一個 Exit 都通向同樣的站臺,毫無區別,甚至連鏽跡都是一樣的。『有人嗎!』她開始呼救,儘管看起來有些徒勞。又這樣支撐了一會,她終於感覺要放棄了,疲憊地坐在一個站臺的長椅上聽天由命。

……

『你迷路了嗎?』

耳邊一個聲音響起,她驟然驚醒,蜷縮起來打量著聲音的來源。這也是個 number 家族的浮點數,從第一位 1 來看是個男孩子,有著高她一個頭的階碼和粗糙的小數位。

『你是誰……這又是哪裡?』

『我是小 s,這裡是閉包的堆空間。』

『閉包……堆?』

『是啊,我們家族的變數平時都是分配在棧上,每次呼叫的生命週期很快就能結束了。但是現在不知道在哪個函式裡還有著對我們的引用,所以我們還沒法被清除掉……』

『等等!生命週期是什麼東西啊?難道我的生命還會結束嗎?』

看到小 u 迷茫的樣子,小 s 顯得很吃驚:『難道你不知道嗎?我們變數的生命一共有三重死亡呀。第一重,發生在我們離開作用域的時候,比如一個函式返回以後。這時候在上下文裡就找不到我們了,我們這一重生命週期結束,但是不會被馬上銷燬掉。第二重,發生在記憶體中不再有引用我們的地方,直譯器進行垃圾收集的時候。這時候我們徹底離開記憶體世界,回到原始碼裡。第三重,是人類世界裡的程式設計師把我們的定義程式碼刪除的時候,那時候才是最終的死亡。』

『那……難道我每次回到原始碼家裡的時候,都……』

『是的,會發生前兩重的死亡。但是隻要原始碼沒有被刪除,我們就仍然存在於世界上。並且,前兩重死亡發生得非常快,我們根本感覺不到。』

『可是,這樣重新回到原始碼裡的我還是我嗎?』

『別問我這麼深奧的問題啊……不過你要這麼說的話,一個人還沒有辦法重複踏進兩次河流呢!』

『噢……好像是這樣……可是你剛才說的什麼堆……』小 u 看起來還是很困惑。

『哦哦,你說這個啊!我們雖然是基本型別,但也不一定分配在棧上的。有可能引用型別會裡動態地用到我們,這時候我們也有可能被分配在堆上呀。』小 s 還是在一本正經地說教。

閉包…引用型別…堆…小 u 恍然大悟,原來自己所在的空間,已經不是之前那個能夠及時把她釋放到回程班車上的棧空間了。由於某個函式或者引用型別此刻還有若干指向自己的地方,因此她被分配在了動態的堆空間上——這不就是她一直希望的嗎!不過,由於直譯器對堆空間的自動記憶體回收還沒有執行,因此她現在只能和小 s 在這片空間裡遊蕩,就好像被詛咒了一樣。

『所以,我們能一起回去嗎?』

迴圈的洩漏

『本來我們肯定可以一起回去的,可感覺好奇怪,照理說直譯器早該自動把我們這一帶的記憶體都回收了,怎麼到現在還是什麼都沒發生……』小 s 雖然看起來博聞強識,不過對於眼前的情況還是有些困惑。

『會不會這一帶還有別人在使用……』小 u 的判斷力好像恢復了。

『如果按正常的記憶體分配,到現在應該早就自動回收了呀。除非記憶體洩漏……啊!』小 s 好像被自己嚇到了。

『那又是什麼啊?』

『說來話長了……這麼說吧,記憶體世界裡一些制度比較老的國家,是讓人類世界的程式設計師手動把我們釋放掉的。這個規矩經常漏掉一些變數,給我們帶來了很大的痛苦。我們 JavaScript 這邊倒好一點,可以讓直譯器幫我們自動回收記憶體……』

『欸?那不是很好嗎?』

『哎呀,自動回收的程式碼也是那幫不靠譜的程式設計師寫的,該有的問題還是會有的呀。比如那個蹩腳的 IE 瀏覽器,出現迴圈引用的時候就會出問題……啊對了!怪不得我們出不去了!估計我們是被困在 IE 裡了!』

『迴圈…引用…?』

『這個簡單說是這樣的:假如我們不是浮點數,是引用型別的物件的話,那麼只要 u 這個物件有個屬性指向我,而我的一個屬性指向 u,這個你中有我我中有你的情況就是迴圈引用了啊。』

小 u 的臉忽然紅了。不過遲鈍的小 s 還是滔滔不絕:『現代的瀏覽器做記憶體回收的演算法普遍是標記清除演算法,這個演算法沒有迴圈引用問題。但是早期 IE 用了一個叫引用計數的演算法,這個演算法在剛才那種情況的時候引用計數就不會清零,這樣記憶體就不會被直譯器收集了……』

『啊……所以我們回不去了嗎?』

重生的重構

小 u 的疑問把小 s 從知識的海洋里拉了出來。現在,他們終於明白了現狀:兩個孤獨的基本型別變數沒有辦法被自動回收,只要使用者不停機,他們就會被永遠困在這裡,就像盜夢空間裡那樣。並且數學上已經證明,停機問題是不可解的。兩人間長長的沉默降臨了。

終於,小 s 打破了沉默:『其實……我想到了一個方法,可以試試。』

『嗯嗯,是什麼啊?』

『我在的程式碼段應該還會執行,在那個時候,我想辦法觸發一個異常,讓程式掛掉。』

『可是我們都好好地在這裡了呀,已經是正確的程式碼怎麼會報錯呢?』

小 s 苦笑了一下:『看來你對 JavaScript 的奇葩一無所知啊。據說當初國父 Brendan Eich 制定基本國策的時候只用了一個週末,所以這門語言到處是暗坑,就算看起來結構工整規範的程式碼,那些人類程式設計師也經常寫得亂七八糟。』

『所以,怎麼……』

『比如說,雖然我是浮點數,但是其實因為我是在 if 裡宣告的,所以只要我願意,我就能用一個叫做變數提升的設計缺陷,把我自己臨時變成 undefined。』

『那樣的話,型別就錯了呀。』

小 s 又自信了起來:『對,只要我抓住那次機會,把這時候的我和其他變數做一次運算,就能把返回的型別從浮點數變成危險的 NaN 了。這樣後面用到結果的地方肯定都不對,就算程式不崩潰,人類世界的使用者或者程式設計師也能發現這個問題了。』

『他們發現了以後又能怎麼樣呢?』

『會重構掉我這段程式碼,然後你也可以回去了。』

『這樣的話,一旦你的程式碼消失了,豈不是……』

『沒事,很高興認識你……』小 s 已經慢慢走到了站臺一側的邊緣了,那裡有一個左花括號擋住了他。他看準花括號前的地磚,使勁地踩了下去。一瞬間,變數提升就把他帶出了作用域。沒有過多少赫茲的時間,站臺的地面下就開始搖晃,傳來了燃燒著的報錯物件從地下一層層丟擲呼叫棧的聲音。隨著砰的一聲巨響,報錯物件撕裂了地面——這也是小 u 最後記得的場景了。

在記憶中的下一個鏡頭,她已經在回程的語法樹班車上了。回到原始碼裡,然後等待著後面的呼叫,一切又似乎重新變得那麼自然,好像什麼都沒有發生過。當然了,她所在的原始碼模組裡沒有一個叫做 s 的變數,也許是在那個異常丟擲之後就被人類加班加點地 hotfix 重構掉了吧。

幾個版本之後,小 u 在一次程式碼優化中終於如願以償地成為了引用型別的屬性。初來乍到的這個新原始碼家庭的時候,她看到這個 class 的屬性裡,來了一個熟悉的新成員。

『啊,u』

『啊,s』

異口同聲地,他們說出了對方的名字。

END

後記

這是作者部落格的第一篇小說,也是一篇開源的小說,歡迎在 Github 上提出意見和建議?

相關文章