弱引用table
與python等腳本語言類似地,Lua也采用了自動內存管理(Garbage Collection),一個程序只需創建對象,而無需刪除對象。通過使用垃圾收集機制,Lua會自動刪除過期對象。垃圾回收機制可以將程序員從C語言中常出現的內存泄漏、引用無效指針等底層bug中解放出來。
我們知道Python的垃圾回收機制使用了引用計數算法,當指向一個對象的所有名字都失效(超出生存期或程序員顯式del了)了,會將該對象占用的內存回收。但對於循環引用是一個特例,垃圾收集器通常無法識別,這樣會導致存在循環引用的對象上的引用計數器永遠不會變為零,也就沒有機會被回收。
一個在python中使用循環引用的例子:
class main1: def __init__(self): print('The main1 constructor is calling...') def __del__(self): print('The main1 destructor is calling....') class main2: def __init__(self, m3, m1): self.m1 = m1 self.m3 = m3 print('The main2 constructor is calling...') def __del__(self): print('The main2 destructor is calling....') class main3: def __init__(self): self.m1 = main1() self.m2 = main2(self, self.m1) print('The main3 constructor is calling...') def __del__(self): print('The main3 destructor is calling....') # test main3()
輸出內容為:
The main1 constructor is calling... The main2 constructor is calling... The main3 constructor is calling...
可以看出,析構函數(__del__函數)沒有被調用,循環引用導致了內存泄漏。
垃圾收集器只能回收那些它認為是垃圾的東西,不會回收那些用戶認為是垃圾的東西。比如那些存儲在全局變量中的對象,即使程序不會再用到它們,但對於Lua來說它們也不是垃圾,除非用戶將這些對象賦值為nil,這樣它們才能被釋放。但有時候,簡單地清除引用還不夠,比如將一個對象放在一個數組中時,它就無法被回收,這是因為即使當前沒有其他地方在使用它,但數組仍引用着它,除非用戶告訴Lua這項引用不應該阻礙此對象的回收,否則Lua是無從得知的。
table中有key和value,這兩者都可以包含任意類型的對象。通常,垃圾收集器不會回收一個可訪問table中作為key或value的對象。也就是說,這些key和value都是強引用,它們會阻止對其所引用對象的回收。在一個弱引用table中,key和value是可以回收的。
弱引用table(weak table)是用戶用來告訴Lua一個引用不應該阻礙對該對象的回收。所謂弱引用,就是一種會被垃圾收集器忽視的對象引用。如果一個對象的引用都是弱引用,該對象也會被回收,並且還可以以某種形式來刪除這些弱引用本身。
弱引用table有3種類型:
1、具有弱引用key的table;
2、具有弱引用value的table;
3、同時具有弱引用key和value的table;
table的弱引用類型是通過其元表中的__mode字段來決定的。這個字段的值應為一個字符串:
如果包含'k',那么這個table的key是弱引用的;
如果包含'v',那么這個table的value是弱引用的;
弱引用table的一個例子,這里使用了collectgarbage函數強制進行一次垃圾收集:
a = {1,4, name='cq'} setmetatable(a, {__mode='k'}) key = {} a[key] = 'key1' key = {} a[key] = 'key2' print("before GC") for k, v in pairs(a) do print(k, '\t', v) end collectgarbage() print("\nafter GC") for k, v in pairs(a) do print(k, '\t', v) end
輸出:
before GC 1 1 2 4 table: 0x167ba70 key1 name cq table: 0x167bac0 key2 after GC 1 1 2 4 name cq table: 0x167bac0 key2
在本例中,第二句賦值key={}會覆蓋第一個key,當收集器運行時,由於沒有地方在引用第一個key,因此第一個key就被回收了,並且table中的相應條目也被刪除了。至於第二個key,變量key仍引用着它,因此它沒有被回收。
注意,弱引用table中只有對象可以被回收,而像數字、字符串和布爾這樣的“值”是不可回收的。
備忘錄(memoize)函數是一種用空間換時間的做法,比如有一個普通的服務器,每當它收到一個請求,就要對代碼字符串調用loadstring,然后再調用編譯好的函數。不過,loadstring是一個昂貴的函數,有些發給服務器的命令有很高的頻率,例如"close()",如果每次收到一個這樣的命令都要調用loadstring,那還不如讓服務器用一個輔助的table記錄下所有調用loadstring的結果。
備忘錄函數的例子:
local results = {} setmetatable(results, {__mode='v'}) function mem_loadstring(s) local res = results[s] if res == nil then res=assert(loadstring(s)) results[s]=res end return res end local a = mem_loadstring("print 'hello'") local b = mem_loadstring("print 'world'") a = nil collectgarbage() for k,v in pairs(results) do print(k, '\t', v) end
例子中,table results會逐漸地積累服務器收到的所有命令及其編譯結果。經過一定時間后,會耗費大量的內存。弱引用table正好可以解決這個問題,如果results table具有弱引用的value,那么每次垃圾收集都會刪除所有在執行時未使用的編譯結果。
在lua元表一文中,提到過如何實現具有默認值的table。如果要為每一個table都設置一個默認值,又不想讓這些默認值持續存在下去,也可以使用弱引用table,如下面的例子:
local defaults = {} setmetatable(defaults, {__mode='k'}) local mt = {__index=function(t) return defaults[t] end} function setDefault(t, d) defaults[t] = d setmetatable(t, mt) end local a = {} local b = {} setDefault(a, "hello") setDefault(b, "world") print(a.key1) print(b.key2) b = nil collectgarbage() for k,v in pairs(defaults) do print(k,'\t',v) end