內存泄漏及檢測方法講解
“該死系統存在內存泄漏問題”,項目中由于各方面因素,總是有人抱怨存在內存泄漏,系統長時間運行之后,可用內存越來越少,甚至導致了某些服務失敗。內存泄漏是最難發現的常見錯誤之一,因為除非用完內存或調用malloc失敗,否則都不會導致任何問題。實際上,使用C/C++這類沒有垃圾回收機制的語言時,你很多時間都花在處理如何正確釋放內存上。如果程序運行時間足夠長,如后臺進程運行在服務器上,只要服務器不宕機就一直運行,一個小小的失誤也會對程序造成重大的影響,如造成某些關鍵服務失敗。
對于內存泄漏,本人深有體會!實習的時候,公司一個項目中就存在內存泄漏問題,項目的代碼兩非常大,后臺進程也比較多,造成內存泄漏的地方比較難找。這次機會是我對如何查找內存泄漏問題,有了一定的經驗,后面自己的做了相關實驗,在此我分享一下內存泄漏如何調試查找,主要內容如下:
- 1、內存泄漏簡介
- 2、Windows平臺下的內存泄漏檢測
- 2.1、檢測是否存在內存泄漏問題
- 2.2、定位具體的內存泄漏地方
- 3、Linux平臺下的內存泄漏檢測
- 4、總結
其實Windows、Linux下面的內存檢測都可以單獨開篇詳細介紹,方法和工具也遠遠不止文中介紹到的,我的方法也不是最優的,如果您有更好的方法,也請您告訴我和大家。
1、內存泄漏簡介及后果
wikipedia中這樣定義內存泄漏:在計算機科學中,內存泄漏指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。
最難捉摸也最難檢測到的錯誤之一是內存泄漏,即未能正確釋放以前分配的內存的 bug。 只發生一次的小的內存泄漏可能不會被注意,但泄漏大量內存的程序或泄漏日益增多的程序可能會表現出各種征兆:從性能不良(并且逐漸降低)到內存完全用盡。 更糟的是,泄漏的程序可能會用掉太多內存,以致另一個程序失敗,而使用戶無從查找問題的真正根源。 此外,即使無害的內存泄漏也可能是其他問題的征兆。
內存泄漏會因為減少可用內存的數量從而降低計算機的性能。最終,在最糟糕的情況下,過多的可用內存被分配掉導致全部或部分設備停止正常工作,或者應用程序崩潰。內存泄漏可能不嚴重,甚至能夠被常規的手段檢測出來。在現代操作系統中,一個應用程序使用的常規內存在程序終止時被釋放。這表示一個短暫運行的應用程序中的內存泄漏不會導致嚴重后果。
在以下情況,內存泄漏導致較嚴重的后果:
- 程序運行后置之不理,并且隨著時間的流失消耗越來越多的內存(比如服務器上的后臺任務,尤其是嵌入式系統中的后臺任務,這些任務可能被運行后很多年內都置之不理);
- 新的內存被頻繁地分配,比如當顯示電腦游戲或動畫視頻畫面時;
- 程序能夠請求未被釋放的內存(比如共享內存),甚至是在程序終止的時候;
- 泄漏在操作系統內部發生;
- 泄漏在系統關鍵驅動中發生;
- 內存非常有限,比如在嵌入式系統或便攜設備中;
- 當運行于一個終止時內存并不自動釋放的操作系統(比如AmigaOS)之上,而且一旦丟失只能通過重啟來恢復。
下面我們通過以下例子來介紹如何檢測內存泄漏問題:
- #include <stdlib.h>
- #include <iostream>
- using namespace std;
- void GetMemory(char *p, int num)
- {
- p = (char*)malloc(sizeof(char) * num);//使用new也能夠檢測出來
- }
- int main(int argc,char** argv)
- {
- char *str = NULL;
- GetMemory(str, 100);
- cout<<"Memory leak test!"<<endl;
- //如果main中存在while循環調用GetMemory
- //那么問題將變得很嚴重
- //while(1){GetMemory(...);}
- return 0;
- }
實際中不可能這么簡單,如果這么簡單也用不著別的方法,程序員一眼就可以看出問題,此程序只用于測試。
2、Windows平臺下的內存泄漏檢測
2.1、檢測是否存在內存泄漏問題
Windows平臺下面Visual Studio 調試器和 C 運行時 (CRT) 庫為我們提供了檢測和識別內存泄漏的有效方法,原理大致如下:內存分配要通過CRT在運行時實現,只要在分配內存和釋放內存時分別做好記錄,程序結束時對比分配內存和釋放內存的記錄就可以確定是不是有內存泄漏。在vs中啟用內存檢測的方法如下:
- STEP1,在程序中包括以下語句: (#include 語句必須采用上文所示順序。 如果更改了順序,所使用的函數可能無法正常工作。)
- #define _CRTDBG_MAP_ALLOC
- #include <stdlib.h>
- #include <crtdbg.h>
通過包括 crtdbg.h,將 malloc 和 free 函數映射到它們的調試版本,即 _malloc_dbg 和 _free_dbg,這兩個函數將跟蹤內存分配和釋放。 此映射只在調試版本(在其中定義了_DEBUG)中發生。 發布版本使用普通的 malloc 和 free 函數。
#define 語句將 CRT 堆函數的基版本映射到對應的“Debug”版本。 并非絕對需要該語句;但如果沒有該語句,內存泄漏轉儲包含的有用信息將較少。
- STEP2, 在添加了上述語句之后,可以通過在程序中包括以下語句(通常應恰好放在程序退出位置之前)來轉儲內存泄漏信息:
- _CrtDumpMemoryLeaks();
此時,完整的代碼如下:
- #define _CRTDBG_MAP_ALLOC
- #include <stdlib.h>
- #include <crtdbg.h>
- #include <iostream>
- using namespace std;
- void GetMemory(char *p, int num)
- {
- p = (char*)malloc(sizeof(char) * num);
- }
- int main(int argc,char** argv)
- {
- char *str = NULL;
- GetMemory(str, 100);
- cout<<"Memory leak test!"<<endl;
- _CrtDumpMemoryLeaks();
- return 0;
- }
當在調試器下運行程序時,_CrtDumpMemoryLeaks 將在“輸出”窗口中顯示內存泄漏信息。 內存泄漏信息如下所示:
如果沒有使用 #define _CRTDBG_MAP_ALLOC 語句,內存泄漏轉儲將如下所示:
未定義 _CRTDBG_MAP_ALLOC 時,所顯示的會是:
- 內存分配編號(在大括號內)。
- 塊類型(普通、客戶端或 CRT)。
- “普通塊”是由程序分配的普通內存。
- “客戶端塊”是由 MFC 程序用于需要析構函數的對象的特殊類型內存塊。 MFC new 操作根據正在創建的對象的需要創建普通塊或客戶端塊。
- “CRT 塊”是由 CRT 庫為自己使用而分配的內存塊。 CRT 庫處理這些塊的釋放,因此您不大可能在內存泄漏報告中看到這些塊,除非出現嚴重錯誤(例如 CRT 庫損壞)。
從不會在內存泄漏信息中看到下面兩種塊類型:
- “可用塊”是已釋放的內存塊。
- “忽略塊”是您已特別標記的塊,因而不出現在內存泄漏報告中。
- 十六進制形式的內存位置。
- 以字節為單位的塊大小。
- 前 16 字節的內容(亦為十六進制)。
定義了 _CRTDBG_MAP_ALLOC 時,還會顯示在其中分配泄漏的內存的文件。 文件名后括號中的數字(本示例中為 10)是該文件中的行號。
注意:如果程序總是在同一位置退出,調用 _CrtDumpMemoryLeaks 將非常容易。 如果程序從多個位置退出,則無需在每個可能退出的位置放置對 _CrtDumpMemoryLeaks 的調用,而可以在程序開始處包含以下調用:
- _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
該語句在程序退出時自動調用 _CrtDumpMemoryLeaks。 必須同時設置 _CRTDBG_ALLOC_MEM_DF 和_CRTDBG_LEAK_CHECK_DF 兩個位域,如前面所示。
2.2、定位具體的內存泄漏地方
通過上面的方法,我們幾乎可以定位到是哪個地方調用內存分配函數malloc和new等,如上例中的GetMemory函數中,即第10行!但是不能定位到,在哪個地方調用GetMemory()導致的內存泄漏,而且在大型項目中可能有很多處調用GetMemory。如何要定位到在哪個地方調用GetMemory導致的內存泄漏?
定位內存泄漏的另一種技術涉及在關鍵點對應用程序的內存狀態拍快照。 CRT 庫提供一種結構類型 _CrtMemState,您可用它存儲內存狀態的快照:
- _CrtMemState s1, s2, s3;
若要在給定點對內存狀態拍快照,請向 _CrtMemCheckpoint 函數傳遞 _CrtMemState 結構。 該函數用當前內存狀態的快照填充此結構:
- _CrtMemCheckpoint( &s1 );
通過向 _CrtMemDumpStatistics 函數傳遞 _CrtMemState 結構,可以在任意點轉儲該結構的內容:
- _CrtMemDumpStatistics( &s1 );
若要確定代碼中某一部分是否發生了內存泄漏,可以在該部分之前和之后對內存狀態拍快照,然后使用 _CrtMemDifference 比較這兩個狀態:
- _CrtMemCheckpoint( &s1 );
- // memory allocations take place here
- _CrtMemCheckpoint( &s2 );
- if ( _CrtMemDifference( &s3, &s1, &s2) )
- _CrtMemDumpStatistics( &s3 );
顧名思義,_CrtMemDifference 比較兩個內存狀態(s1 和 s2),生成這兩個狀態之間差異的結果(s3)。 在程序的開始和結尾放置 _CrtMemCheckpoint 調用,并使用_CrtMemDifference 比較結果,是檢查內存泄漏的另一種方法。 如果檢測到泄漏,則可以使用 _CrtMemCheckpoint 調用通過二進制搜索技術來劃分程序和定位泄漏。
如上面的例子程序我們可以這樣來定位確切的調用GetMemory的地方:
- #define _CRTDBG_MAP_ALLOC
- #include <stdlib.h>
- #include <crtdbg.h>
- #include <iostream>
- using namespace std;
- _CrtMemState s1, s2, s3;
- void GetMemory(char *p, int num)
- {
- p = (char*)malloc(sizeof(char) * num);
- }
- int main(int argc,char** argv)
- {
- _CrtMemCheckpoint( &s1 );
- char *str = NULL;
- GetMemory(str, 100);
- _CrtMemCheckpoint( &s2 );
- if ( _CrtMemDifference( &s3, &s1, &s2) )
- _CrtMemDumpStatistics( &s3 );
- cout<<"Memory leak test!"<<endl;
- _CrtDumpMemoryLeaks();
- return 0;
- }
調試時,程序輸出如下結果:
這說明在s1和s2之間存在內存泄漏!!!如果GetMemory不是在s1和s2之間調用,那么就不會有信息輸出。
3、Linux平臺下的內存泄漏檢測
在上面我們介紹了,vs中在代碼中“包含crtdbg.h,將 malloc 和 free 函數映射到它們的調試版本,即 _malloc_dbg 和 _free_dbg,這兩個函數將跟蹤內存分配和釋放。 此映射只在調試版本(在其中定義了_DEBUG)中發生。 發布版本使用普通的 malloc 和 free 函數。”即為malloc和free做了鉤子,用于記錄內存分配信息。
Linux下面也有原理相同的方法——mtrace,http://en.wikipedia.org/wiki/Mtrace。方法類似,我這就不具體描述,參加給出的鏈接。這節我主要介紹一個非常強大的工具valgrind。如下圖所示:
如上圖所示知道:
- ==6118== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
- ==6118== at 0x4024F20: malloc (vg_replace_malloc.c:236)
- ==6118== by 0x8048724: GetMemory(char*, int) (in /home/netsky/workspace/a.out)
- ==6118== by 0x804874E: main (in /home/netsky/workspace/a.out)
是在main中調用了GetMemory導致的內存泄漏,GetMemory中是調用了malloc導致泄漏了100字節的內存。
- Things to notice:
- • There is a lot of information in each error message; read it carefully.
- • The 6118 is the process ID; it’s usually unimportant.
- • The first line ("Heap Summary") tells you what kind of error it is.
- • Below the first line is a stack trace telling you where the problem occurred. Stack traces can get quite large, and be
- confusing, especially if you are using the C++ STL. Reading them from the bottom up can help.
- • The code addresses (eg. 0x4024F20) are usually unimportant, but occasionally crucial for tracking down weirder
- bugs.
- The stack trace tells you where the leaked memory was allocated. Memcheck cannot tell you why the memory leaked,
- unfortunately. (Ignore the "vg_replace_malloc.c", that’s an implementation detail.)
- There are several kinds of leaks; the two most important categories are:
- • "definitely lost": your program is leaking memory -- fix it!
- • "probably lost": your program is leaking memory, unless you’re doing funny things with pointers (such as moving
- them to point to the middle of a heap block)
Valgrind的使用請見手冊http://valgrind.org/docs/manual/manual.html。
4、總結
其實內存泄漏的原因可以概括為:調用了malloc/new等內存申請的操作,但缺少了對應的free/delete,總之就是,malloc/new比free/delete的數量多。我們在編程時需要注意這點,保證每個malloc都有對應的free,每個new都有對應的deleted!!!平時要養成這樣一個好的習慣。
要避免內存泄漏可以總結為以下幾點:
程序員要養成良好習慣,保證malloc/new和free/delete匹配;
檢測內存泄漏的關鍵原理就是,檢查malloc/new和free/delete是否匹配,一些工具也就是這個原理。要做到這點,就是利用宏或者鉤子,在用戶程序與運行庫之間加了一層,用于記錄內存分配情況。