淺談動態語言與垃圾回收

相信對於很多技術愛好者而言,第一次聽說complier's compiler時,會有一種興奮,會有一種衝動,想要知道一門語言是如何實現的,想要親自動手實現一門自己的語言,那樣真的很技術!所以,馬上度娘學習資料,此時,龍書、虎書、鯨書進入了視野。

一段時期的充能,一段時期的積澱,一段時期的構思,一段時期的精雕細琢,最終完成了一個腳本語言的雛形,此時,或許你已然將垃圾回收拋諸腦後,抑或尚不明白垃圾回收的真實作用。

有些人喜歡將引用計數作為一種垃圾回收算法,然而碼哥並不這樣認為。

在腳本實現中,變量釋放的時機是有講究的(可參見後面對象釋放的部分)。之所以要考慮釋放時機,是因為存在引用關係。之所以存在引用關係,是因為存在數組、對象等這樣用到引用關係的複雜結構。而之所以需要這些複雜結構,是因為一個用於生產環境的語言必須包含這些結構,否則無法實現所需邏輯。

因此,碼哥認為引用計數是一種必要機制,而不是一種可有可無可被替代的算法。僅僅實現了引用計數的語言,我亦稱其為無垃圾回收語言。

無垃圾回收的世界

我們以python為例,來看看一個無垃圾回收的世界是如何的。

<code>import gc 


gc.disable()

class Model:
def __init__(self):
self.a = None

while 1:
o = Model()
o.a = 1/<code>

這裡,我們讓程序陷入死循環。打開top,可以看到這個程序cpu幾近打滿,而內存極低並不增長,似乎一切都很符合我們的預期。

這裡要明白,對o的重複賦值代表了將以前的值丟棄,重新創建了一個新值。

然而,眼前的順利恰好說明,語言的創建者並未嘗試利用語言構建複雜邏輯。

例如,我們將最後一行代碼替換為:

<code>o.a = o/<code>

此時再次運行,就會看到內存暴漲。

此刻到底發生了什麼呢?

循環引用

我們來梳理一下:

對於腳本語言,變量名和變量實際的值是分開的,這樣便於應對引用需求。以對象o為例,其名為o,其值為一對象結構(我們暫且稱其為s)。這個對象結構s有引用計數字段。當這個值結構s賦給o這個名字時,其引用計數為1,表示名字o與對象結構s關聯了。

而當o.a = o時,對象結構s引用又將增1,表示對象o的名為a的成員與該對象結構s進行了關聯,此時引用為2。

到此,一切都很合理。

而作為一個對象,s的釋放必然是先將其引用計數降低至0,然後才可以真正釋放其內部成員。原因很簡單,如果提前釋放成員,結果發現結構s的引用依舊未歸零,那麼已釋放的部分是無法恢復的。因此對象結構s的引用計數為2而無法被釋放。

這種情況叫做循環引用

當然,有時循環引用並不這麼直觀出現,它可能會以如下形式甚至更復雜的形式出現:

<code>while 1:
o = Model()
p = Model()
o.a = p
p.a = o/<code>

垃圾回收的意義

垃圾回收的真正意義,就在於回收循環引用的變量。因為非循環引用的狀況下,數據都可以立刻釋放,因此無需使用額外的垃圾回收機制檢測。

在上面的例子中,或許有些人會馬上想到,我們可以先遍歷對象結構s的每個成員,看每個成員對s是否有引用,引用了多少次,就可以知道是否能夠釋放s了。

沒錯,事實上垃圾回收也正是在檢查這些內容。在實際的工程中,對象套對象,數組套對象,對象套數組,數組套數組,這將產生諸多不可預計的組合層次。由此可想而知,垃圾回收在這樣的情形下會耗費多少算力和時間。

結束語

一門真正的動態語言與垃圾回收是相互依賴,而不是可替代可無視的關係。單純談論垃圾回收機制並不能幫助人們更好的瞭解為何需要垃圾回收。

此外,由上面的討論我們也看到,一門動態語言的性能,不僅在於其實現機制,也在於編寫者的代碼邏輯。


分享到:


相關文章: