前言
一般來說.NET程序員是不用擔心內存分配釋放問題的,因為有垃圾收集器(GC)會自動幫你處理。但是GC只能收集那些不再使用的內存(根據對象是否被其它活動的對象所引用)來確定。所以如果代碼編寫不當的話,仍然會出現內存泄漏的問題,常見的情況有:一個靜態變量引用了一個應該被釋放的對象,事件注冊后不解除注冊,非托管資源使用后沒有手動釋放。不斷的內存泄漏終會引起內存不足,掛掉你的程序。
對於這種內存泄漏問題,有很多的分析工具可以使用,常見的有CLRProfiler、ANTS Performance Profiler等。不過從vs2013起,vs自帶了一個分析工具-Diagnostic Tool,默認debug時會自動打開,如果沒有打開的話就按快捷鍵Ctrl+Alt+F2。
如下圖:
開始使用
這里以靜態變量持有應釋放的對象為例,簡單介紹下使用方法。
這里有個winform程序,主要功能即使點擊按鈕后,輸出名為“jim”的個人信息:
private void button1_Click(object sender, EventArgs e) { var p = PersonManager.Get("jim"); label1.Text += p.Name + " " + p.Age + "\n"; }
Person和PersonManager類的信息如下:
namespace MemLeak { public class Person { public string Name { get; set; } public int Age { get; set; } public byte[] BinaryData { get; set; } } public static class PersonManager { static List<Person> people = new List<Person>(); public static List<Person> People { get { return people; } } public static Person Get(string name) { //正常邏輯 //var per = People.FirstOrDefault(o => o.Name == name); //if(per==null) //{ // per = new Person(); // per.Age = 23; // per.Name = name; // per.BinaryData = File.ReadAllBytes(@"D:\2.zip");//一個38MB的文件 // people.Add(per); //} //return per; //錯誤邏輯 Person p = new Person(); p.Age = 23; p.Name = name; p.BinaryData = File.ReadAllBytes(@"D:\2.zip");//一個38MB的文件 people.Add(p); return p; } } }
1. 捕獲內存快照
程序運行起來后,從下圖可以看到內存占用大約在18M左右,此時點擊“截取快照”新建一個快照:
然后click按鈕5次,然后再點擊 截圖快照 ,如下:
從曲線圖中可以明顯看到內存從18M逐步增長到227M。第二次快照與第一次相比對象增加了19個,堆上的內存使用增加了191M左右。
點擊圖中的(+19)字樣,可以進入到快照詳細對比界面。
2. 詳細對比
如圖所示,我們先來看下選中的那一行是什么意思:
可以看到MemLeak.Person
這個對象的計數差異為+5,計數是5。表示這兩次快照之間這個Person
對象從0個變成了5個增加了5個。大小差異是增加了195601040個字節,這幾個從字面意思看都很好理解。唯一有點難度的可能是非獨占大小(字節)
,一眼看不出來是干什么的。但是我們注意到了List<MemLeak.Person>
對象的大小是68字節而非獨占大小卻是195601108字節。
非獨占大小的統計方式如下:
其中最下層的string
和byte[]
,由於沒有子節點了,所以它們的非獨占大小就是它們本身的大小。Address
的非獨占大小=自己本身的大小+String
的非獨占大小(50字節)+Byte[]的非獨占大小(100字節)=200字節。Customer
的非獨占大小=自己本身(100字節)+String
的非獨占大小(100字節)+Address
的非獨占大小(200字節)=400字節。
所以這也就解釋了List<MemLeak.Person>
,自身大小僅是68字節,但是由於它是一個List內部包含了5個Person
對象,所以它的非獨占大小就是5個Person
對象大小的和。
3. 哪里產生了泄漏?
這里我先將過濾器里“僅我的代碼”和“折疊小型對象類型”前面的勾去掉,便於分析。
因為我們值需要看當前程序集(MemLeak)的內存信息,所以在搜索類型名的框里直接輸入我們程序集的名稱,將其他一些沒用的過濾掉。
是不是清爽了很多!
分析內存當然要拿占用內存增多最大的對象開刀,點擊MemLeak.Person
,在下面的“根的路徑”視圖中可以看到增加的5個Person
對象是在一個Person[]
數組中。
但是我們好像並沒有聲明過這樣的數組,所以繼續展開:
原來是在一個List
里面,而且這個List
還是靜態的。這時候就要想到內存泄漏的原因,可能就是代碼哪里的靜態變量持續占有應該釋放的對象了。然后更改為正確代碼就OK了。
4. 查看對象中哪種數據類型占用內存較多?
切換到的引用選項卡(“引用的類型”)。
可以看到本例中Person
的Byte[]
類型的占用內存較多,所以我們就可以重點排查Person
中哪里用到了Byte[]
類型,進而去優化。
當初取消勾選的那兩個選項是什么?
第一個是“僅我的代碼”:
勾上之后,會過濾掉那些.Net Runtime產生的一些對象和一些很常見的系統認為與你的程序無關的一些對象。
第二個是:“折疊小的對象類型”:
勾上后,會過隱藏掉那些非獨占大小小於托管堆總大小0.5%的對象。將這些小對象的非獨占大小合並的父節點中。如下:
這兩個選項默認都勾上即可。
其它對於生產環境中的軟件,分析內存時可以多次抓取dump文件,用vs打開后分析比對。
不過只有vs的企業版才有這個功能。
參考