關於 Lua 內存泄漏的檢測


  前一陣開始和同事一起優化內存,首先是優化 Lua 內存,因為發現每次戰斗完后 Lua 內存非常大,從 3M 左右在經過了10次左右的戰斗后,會暴增到近 100M,很明顯是有內存泄漏。
     然后我正式啟動該工作,基本思路就是遞歸遍歷內存中所有的數據,表,函數,協程,用戶數據,查看未釋放和筆誤引起的全局變量泄漏;於是通過搜索我參考了以下資料:
   Lua 官方手冊(最重要)
 
     以上資料有各自的參考價值,但是也有些不正確和不符合我要求的地方,一是搜索的根節點不是從 _G 開始,而是從 debug.getregistry 開始,否則你會遺漏很多數據;二是我不想用 c 寫,而是直接用 lua 實現,把結果打印到 txt 里。
     首先我的搜索方式如下以遞歸的方式進行:
 
 
  • 對搜索到的每一個數據進行引用計數並放置在 weak table 中。
  • 查找全局變量泄漏:啟動游戲打印一份完整的游戲數據,游戲退出前打印一份完整的內存數據,然后把差異的部分再過濾輸出並且按照引用次數進行排序,然后逐個查找所有可疑或者不該出現的全局變量(一般都在根節點),直接定位修改代碼,直到沒有全局變量泄漏位置。
  • 查找游戲邏輯數據未釋放:比如查找戰斗邏輯泄漏,在每次進入主場景打印一份完整的數據,這樣每次戰斗完成都會回到主場景,而且理論上回到主場景戰斗數據都是必須釋放的,然后對比最近兩次主場景中打印的內存數據,將差異部分輸出並且按照引用次數排序,然后根據結果優化或者修改代碼邏輯,將沒有釋放的地方進行釋放。
  • 不斷地循環以上方式,直到內存穩定且總量在合理預期范圍內。
     通過以上方式,解決了項目中的 Lua 內存泄漏,長時間連續游戲,Lua 內存穩定在 3.5M 左右,高峰時 會到 5M。
 

  
   2017-05-05 更新:
  (如果你發現它對你有所幫助,請貢獻一個友好的 Star 吧:)
 
  應有些朋友的要求建立一個QQ群,大家可以自行交流工具使用和其它技術,所以我建了一個,QQ群號:330366204。
 

  
   2017-07-01 更新
  多說兩句關於 Lua 內存泄露,之前這個項目出現如此離譜的情況是因為全局變量的問題,這個工具對於查找全局變量的內存泄露很有效。所以對於 Lua 5.1,應該主動對所有的邏輯腳本封裝沙盒,Lua 5.3 的 _ENV 有效的改善了這個問題,但是也應該進一步封裝邏輯腳本的沙盒,這樣邏輯開發人員再怎么寫全局變量也不會出問題,唯一容易內存泄露的地方就是大量引用了 C# 端的游戲對象和各種腳本,以此鏈接了大量的對象和資源等等再經歷了很多游戲場景后依然無法得到有效的釋放。
 

  
  2017-07-21 更新
  很開心這個工具分享后幫助一些朋友解決了問題,但是在跟一些朋友溝通中發現有些重要的接口方法沒有用上,或者工具使用不正確,為此我徹底更新了 GitHub 頁面上的使用說明(英文)。在這里再簡單說下工具使用。
  首先 require 這個腳本,例如:local mri = require(MemoryReferenceInfo)
  然后在某個地方打印一份內存快照:
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshot("./", "All", -1)

  快照文件的內容,每一行是一個引用對象信息,所有的信息按照引用次數降序排列,每一行被 tab 分成了3列,分別是:對象類型/地址,引用鏈,引用次數。整個文件可以使用 Excel 打開,會自動歸為3列,方便閱讀,重新排序。

  文件內容中重點部分是引用鏈的信息,例如 "function: 0x7f85f8e0e3f0 registry.2[_G].Author.Ask[line:33@file:example.lua] 1" 這條信息說明的是:表 "registry" 的成員 "2"(也就是表 "_G")引用了表 "Author",表 "Author" 有一個成員 "Ask" 引用了 "function: 0x7f85f8e0e3f0",函數位置在文件 "example.lua" 中的第33行,一共被引用了1次。這樣就能快速的定位什么對象在哪里被引用,一共被引用了多少次。

  "DumpMemorySnapshot" 這個方法最后兩個參數是“根節點對象名稱“和“搜索根節點對象”,默認值為 "registry" 和 "debug.getregistry()",在大多數使用的時候不需要修改使用默認值即可,但是當你想從別的根節點開始搜索來縮小范圍,例如從 "_G" 來搜索,你可以手動設置這兩個參數,例如:

-- Only dump memory snapshot searched from "_G".
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshot("./", "All", -1, "_G", _G)

  當整個程序運行一段時間后,再打印一份內存快照(可以打印多份),接下來最重要的工作就是對比快照分析增加的泄露點。在這個工具中,提供了一個名為 “DumpMemorySnapshotComparedFile” 的接口來實現這個對比功能,切記不要自己用文件對比工具來對比兩份快照(有朋友這樣用過),因為快照內容是根據引用計數來降序排序的,時間不同內容也不同,順序也不同,所以普通的文件對比工具在這里是無法生效的。使用方法:

mri.m_cMethods.DumpMemorySnapshotComparedFile("./", "Compared", -1, 
"./LuaMemRefInfo-All-[1-Before].txt", 
"./LuaMemRefInfo-All-[2-After].txt")

這個方法會生成一個新文件,里面是出現在第二份快照里但是沒有並出現在第一份快照里的數據,這就是新增內容。

  無論是那種類型的數據,如果 dump 后數據過大,但是想查看某個特定的數據,可以使用過濾器來生成一個新文件,可以選擇新文件生成的內容是包含關鍵字,還是排除關鍵字,例如:

-- 輸出文件里所有包含關鍵字 “Author” 的內容。(不區分大小寫)
mri.m_cBases.OutputFilteredResult("./LuaMemRefInfo-All-[2-After].txt", "Author", true, true)

--輸出文件里所有不包含關鍵字 “Author” 的內容。(不區分大小寫)
-- Filter all result exclude keywords: "Author".
mri.m_cBases.OutputFilteredResult("./LuaMemRefInfo-All-[2-After].txt", "Author", false, true)

  另外,如果想查看某個對象到底被哪些地方引用着,可以使用接口 "DumpMemorySnapshotSingleObject",例如:

--輸出所有引用對象 "_G.Author" 的地方。 
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshotSingleObject("./", "SingleObjRef-Object", -1, "Author", _G.Author)

-- 輸出所有引用字符串 "yaukeywang" 的地方。
collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshotSingleObject("./", "SingleObjRef-String", -1, "Author Name", "yaukeywang")

  通過以上幾個主要的方法配合使用,就可以快速的查出內存泄漏,即使再手機上也可以使用,比如打印時將保存路徑指向 sd 卡目錄,例如如果使用 Unity 里的 Lua,可以使用:

collectgarbage("collect")
mri.m_cMethods.DumpMemorySnapshot(UnityEngine.Application.persistentPath, "All", -1)

它將輸出一份快照文件到 sd 卡目錄下。

  現在新加了一個配置選項,一般例如 "DumpMemorySnapshot" 這個方法都是指定一個保存路徑和額外信息,然后保存的文件名最后每次都會加上當前的時間戳,方便根據時間來區分不同的快照,也避免需要頻繁的設置和修改文件名,也避免同一個地方不同時間的快照被不斷覆蓋,這個時間戳選項默認開啟,可以通過 "mri.m_cConfig.m_bAllMemoryRefFileAddTime = false" 來關閉,配置的設置放置在 "require" 后,Dump 之前,其它幾個 Dump 的接口也都有是否附加時間戳到文件名的選項,具體參看源碼。

  除了以上的方法,還提供了一些其它的接口可以使用,更詳細的使用請參考 GitHub 上的 ReadMe 和源碼中的接口定義說明,都寫的很詳細了,"Example.lua" 中也演示了常用接口的使用方法。

  最后,最近完善了下這個工具,增加了字符串類型的輸出,所以上面的那張搜索路徑圖,路徑上可以再添加一個 "string"。同時需要注意:為了能在同一行顯示所有字符串(以方便其他方法對數據進行處理,例如對比差異增量,Excel 排序統計等),字符串在顯示的時候所有的回車和換行符:'\r', '\n' 都被顯示的替換成了 '\\n',需要閱讀數據的時候注意。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM