lua記憶體泄露
首先第一點,lua中的記憶體泄露和我們所說的c/c++中的記憶體泄露本質上是不一樣的。lua中有記憶體回收機制(GC),所以理論上是不會有記憶體泄露的。當它進行GC的時候,會從根部開始掃描所有的對象,如果某個地方對這個對象還有引用,就不會把這個對象記憶體collect,這個對象就沒有被GC。所以lua中的記憶體泄露是指那些:已經沒有被使用了,但外部依然還有引用存在的對象。
[plain] view plain copy --函數中應該被申明為local的對象忘記加local local function test() testTable = {} --這個testTabel會被存放在<span style="background-color: rgb(255, 255, 0);">全域表_G</span>中,GC時由於此對象還有引用存在,所以這裡總是會有一個table泄露。 local mt = {} --mt加了local修飾,函數調用完後,引用也不複存在了,GC時會被回收。 setmetatable(testTable, mt) end
以下是一些常見的錯誤引用情景:
1. 本應該local 的變數進入global空間或者module空間了(忘記寫local),如果
這是一個table/function/udata等類型的變數的話,非常不幸的,這個變數將不會
被正確gc了 ----除非你再顯式的釋放。這是非常容易犯的錯誤,一直在想為什麼
lua變數不是預設local呢。 當然這個話題會引發另外一場爭論。
local function test_user(id)
userobj = get_user_by_id(id) --這裡總是會有一個玩家對象泄漏
print("only test", userobj:get_name())
end
2. c/c++部分調用的lua_ref是否有正常lua_unref釋放。 通過
debug.getregistry()可以查到這些ref.
3. 其他各種各樣的實際bug造成的泄漏。
解決方案:
可以建立一個weak table, 把你所有建立過的能夠稱之為資源的,包含但不限於“戰鬥對象,玩家,npc,物品,情境,郵件”等等對象全部扔到這個table裡面。當你知道玩家
已經下線、戰鬥已經銷毀了,但通過連續的強制full gc以後weak table裡面還有這個變數,這就證明了這個變數的引用沒有被完全釋放,
知道有泄漏是比較容易的,能夠完全揪出來就不是很容易了。是的,它究竟在哪兒呢? 一開始在此項目裡面也是先發現比如某npc泄漏了,然後就去查代碼,看看究竟哪個地方寫得不對。這種方式效率極低,基本上查不到什麼問題。在遲一點的時候才使用現在的方案:從_G深度遍曆所有的table、metatable、funciton's upvalue、function's env、registentry(lua_ref)。 目前所知的所有引用必定存在於這幾個空間, 遍曆完成以後一定可以找到那個“迷失了的引用”。 這種方式在指令碼層就可以完成所有事情,甚至你可以在運營環境中線上查證,其遍曆的速度是非常快的,但記憶體開銷非常大(:,可以考慮一邊遍曆一邊gc,當然還要記得避免重複搜尋。 在應用此方案以後,此項目解決了指令碼中所有的泄漏問題。
檢測原理
lua中支援記憶體回收機制的對象有五種:string,table,function,full userdata,thread。而他們的引用直接或間接的儲存到:lua_state對象,_G全域表,Registry註冊表,global_state->mt中。
在指令碼中: 啟動並執行lua指令碼本身就是lua_state。_G就是_G全域表。Registry表可以用debug.getregistry擷取。global_mt可以用debug.getmetatable擷取。所以我們就可以在指令碼層次實現記憶體泄露的檢測模組。
在搜尋時需要注意的幾點: table 額外搜尋metatable,若metatable中的__mode取值為”k"、"v"或者”kv"需特殊處理(補充中有說明); function 額外搜尋 enviroment,也是一個table; 額外搜尋upvalues,這個可以是任何類型。由於userdata在script層次不能被修改,所以搜搜他的metatable吧thread對象就是coroutine對象,在script中一般都不會建立多個coroutine,所以在指令碼中沒搜尋它。若是需求的話,擷取到它的線程函數,然後再按照第2步操作就可以了。 搜尋流程圖(_G表)
檢測泄露之前,先搜尋一下所有的對象,儲存好起始的記憶體狀態,在程式執行之後執行幾次GC操作,然後再進行一次搜尋,對比兩次的結果,多出來的那些就有可能是記憶體泄露了。
__mode 賦值為 "k", "v"或者”kv",表示儲存在它中的鍵或值或索引值都是一種弱引用狀態。若一個對象的所有引用都是弱引用了,那麼這個對象也會被GC回收掉,所以對應的weak表中此對象的入口就沒有了。
所以我們可以用另外一種實現:就是把使用者自己建立的資來源物件統統都丟到weak表中,運行完程式後強制GC,然後去查看weak表,若表中還儲存著那個對象,就意味著這個對象還有外部參考(相對弱引用我們就叫它為強引用吧),資源沒有被GC掉,所以我們可以說這個對象很有可能是記憶體泄露了。( 為了發現記憶體流失,我們可以建立一個全域的弱引用table,使其key為弱引用,然後在每次建立那些可能存在泄漏的對象的時候,都放入這個table,讓其作為key,value通常我會用目前時間。由於弱引用的性質,如果其他引用都消失了,那麼在弱引用table中對這個對象的引用也會消失(變成nil),反之,只要還有其它任何一個引用存在,這個弱參考資料表中對這個對象的引用就繼續存在。依賴這個特性,當程式已經跑過釋放對象的邏輯後,如果這個表中還存在有這個對象的引用,那麼這個對象肯定就是泄漏了。)
Lua記憶體回收演算法
Lua的GC演算法使用的所謂“Mark And Sweep”演算法。簡單的理解,這個演算法將GC分為兩個階段,一個是標記(mark)階段,這一階段將所有系統中引用的對象都逐一標記;而在清理(sweep)階段,將把在mark階段中沒有被標記的資料刪除。
在Lua中,使用幾種顏色來區分不同的結點:
white:白色表示沒有進行過標記的節點
gray:灰色表示已經進行過標記的節點,但是與它相關聯的節點還沒有進行過標記。
black:本節點和與之關聯的節點都已經被掃描標記過了。通常會出現有關聯資料的,包括有Table,upvalue等資料類型。 垃圾收集器函數
collectgarbage函數提供了多項功能:停止記憶體回收,重啟記憶體回收,強制執行一次回收迴圈,強制執行一步記憶體回收,擷取Lua佔用的記憶體,以及兩個影響記憶體回收頻率和步幅的參數。collectgarbage(opt,[,arg])
| "stop" |
停止垃圾收集器,如果它的運行。 |
| "restart" |
如果垃圾收集器已經停止,將重新啟動它。 |
| "collect" |
執行一次全垃圾收集迴圈。預設執行此操作 |
| "count" |
返回當前Lua中使用的記憶體量(以KB為單位) |
| "step" |
逐步執行一個垃圾收集. 步長 "Size" 由參數arg指定 (大型的值需要多步才能完成),如果要準確指定步長,需要多次實驗以達最優效果。如果步長完成一次收集迴圈,將返回True |
| "setpause" |
設定 arg/100 的值作為暫訂收集的時間長度;並返回設定前的值。預設為200 控制了收集器在開始一個新的收集周期之前要等待多久。 隨著數位增大就導致收集器工作工作的不那麼主動。 小於 1 的值意味著收集器在新的周期開始時不再等待。 當值為 2 的時候意味著在總使用記憶體數量達到原來的兩倍時再開啟新的周期。 |
| "setstepmul" |
設定 arg/100 的值,作為步長的增幅(即新步長=舊步長*arg/100);並返回設定前的值。預設為200 控制了收集器的工作速度,這個速度是一個相對於記憶體配置的速度。更大的數字將導致收集器工作的更主動的同時,也使每步收集的尺寸增加。 小於 1 的值會使收集器工作的非常慢,可能導致收集器永遠都結束不了當前周期。 預設值為200%,這意味著收集器將以記憶體 Clerk的兩倍速運行。 |
[plain] view plain copy function test1() collectgarbage("collect")--為了有乾淨的環境,先把可以收集的垃圾收集了 collectgarbage()--為了保證記憶體的收集的相對乾淨,及記憶體的穩定,要執行多次收集 print("now,Lua記憶體為:",collectgarbage("count")) -->205.7158203125 KB local colen = {} --現在是局部變數 for i=1,5000 do table.insert(colen,{}) end print("now,Lua記憶體為:",collectgarbage("count"))-->860.4111328125 KB --建立5000個table,記憶體增加了655 KB end function collect1() print("now,Lua記憶體為:",collectgarbage("count"))-->608.060546875 KB collectgarbage() collectgarbage() print("now,Lua記憶體為:",collectgarbage("count"))-->204.8408203125 KB --最後與一開始只差只有1KB end function test2() collectgarbage("collect")--為了有乾淨的環境,先把可以收集的垃圾收集了 collectgarbage()--為了保證記憶體的收集的相對乾淨,及記憶體的穩定,要執行多次收集 print("now,Lua記憶體為:",collectgarbage("count")) -->205.7158203125 KB colen = {} --現在是全域變數 for i=1,5000 do table.insert(colen,{}) end print("now,Lua記憶體為:",collectgarbage("count"))-->619.826171875 KB --建立5000個table,記憶體增加了414 KB;這些增加的記憶體,由於已放到了全域函數中,是永遠沒有機會被回收到了! end function collect2() print("now,Lua記憶體為:",collectgarbage("count"))-->596.7822265625 KB collectgarbage() collectgarbage() collectgarbage() print("now,Lua記憶體為:",collectgarbage("count"))-->489.189453125 KB --最後記憶體增加了284KB(489-205) end
記憶體回收行程有兩個參數用於控制它的節奏:
第一個參數,稱為暫停時間,控制回收器在完成一次回收之後和開始下次回收之前要等待多久;
第二個參數,稱為步進係數,控制回收器每個步進回收多少內容。粗略地來說,暫停時間越小、步進係數越大,記憶體回收越快。這些參數對於程式的總體效能的影響難以預測,更快的記憶體回收行程顯然會浪費更多的CPU周期,但是它會降低程式的記憶體消耗總量,並可能因此減少分頁。只有謹慎地測試才能給你最佳的參數值。 [轉自]http://www.2cto.com/kf/201502/377646.html
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Lua教程之弱引用table
這次要介紹的內容比較少,就一個——弱引用table
1.無法超越人類智慧的智能——自動記憶體管理的缺陷
我們都知道,Lua是具備自動記憶體管理