內(nèi)存泄漏及檢測方法講解
“該死系統(tǒng)存在內(nèi)存泄漏問題”,項(xiàng)目中由于各方面因素,總是有人抱怨存在內(nèi)存泄漏,系統(tǒng)長時(shí)間運(yùn)行之后,可用內(nèi)存越來越少,甚至導(dǎo)致了某些服務(wù)失敗。內(nèi)存泄漏是最難發(fā)現(xiàn)的常見錯(cuò)誤之一,因?yàn)槌怯猛陜?nèi)存或調(diào)用malloc失敗,否則都不會導(dǎo)致任何問題。實(shí)際上,使用C/C++這類沒有垃圾回收機(jī)制的語言時(shí),你很多時(shí)間都花在處理如何正確釋放內(nèi)存上。如果程序運(yùn)行時(shí)間足夠長,如后臺進(jìn)程運(yùn)行在服務(wù)器上,只要服務(wù)器不宕機(jī)就一直運(yùn)行,一個(gè)小小的失誤也會對程序造成重大的影響,如造成某些關(guān)鍵服務(wù)失敗。
對于內(nèi)存泄漏,本人深有體會!實(shí)習(xí)的時(shí)候,公司一個(gè)項(xiàng)目中就存在內(nèi)存泄漏問題,項(xiàng)目的代碼兩非常大,后臺進(jìn)程也比較多,造成內(nèi)存泄漏的地方比較難找。這次機(jī)會是我對如何查找內(nèi)存泄漏問題,有了一定的經(jīng)驗(yàn),后面自己的做了相關(guān)實(shí)驗(yàn),在此我分享一下內(nèi)存泄漏如何調(diào)試查找,主要內(nèi)容如下:
- 1、內(nèi)存泄漏簡介
- 2、Windows平臺下的內(nèi)存泄漏檢測
- 2.1、檢測是否存在內(nèi)存泄漏問題
- 2.2、定位具體的內(nèi)存泄漏地方
- 3、Linux平臺下的內(nèi)存泄漏檢測
- 4、總結(jié)
其實(shí)Windows、Linux下面的內(nèi)存檢測都可以單獨(dú)開篇詳細(xì)介紹,方法和工具也遠(yuǎn)遠(yuǎn)不止文中介紹到的,我的方法也不是最優(yōu)的,如果您有更好的方法,也請您告訴我和大家。
1、內(nèi)存泄漏簡介及后果
wikipedia中這樣定義內(nèi)存泄漏:在計(jì)算機(jī)科學(xué)中,內(nèi)存泄漏指由于疏忽或錯(cuò)誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。內(nèi)存泄漏并非指內(nèi)存在物理上的消失,而是應(yīng)用程序分配某段內(nèi)存后,由于設(shè)計(jì)錯(cuò)誤,導(dǎo)致在釋放該段內(nèi)存之前就失去了對該段內(nèi)存的控制,從而造成了內(nèi)存的浪費(fèi)。
最難捉摸也最難檢測到的錯(cuò)誤之一是內(nèi)存泄漏,即未能正確釋放以前分配的內(nèi)存的 bug。 只發(fā)生一次的小的內(nèi)存泄漏可能不會被注意,但泄漏大量內(nèi)存的程序或泄漏日益增多的程序可能會表現(xiàn)出各種征兆:從性能不良(并且逐漸降低)到內(nèi)存完全用盡。 更糟的是,泄漏的程序可能會用掉太多內(nèi)存,以致另一個(gè)程序失敗,而使用戶無從查找問題的真正根源。 此外,即使無害的內(nèi)存泄漏也可能是其他問題的征兆。
內(nèi)存泄漏會因?yàn)闇p少可用內(nèi)存的數(shù)量從而降低計(jì)算機(jī)的性能。最終,在最糟糕的情況下,過多的可用內(nèi)存被分配掉導(dǎo)致全部或部分設(shè)備停止正常工作,或者應(yīng)用程序崩潰。內(nèi)存泄漏可能不嚴(yán)重,甚至能夠被常規(guī)的手段檢測出來。在現(xiàn)代操作系統(tǒng)中,一個(gè)應(yīng)用程序使用的常規(guī)內(nèi)存在程序終止時(shí)被釋放。這表示一個(gè)短暫運(yùn)行的應(yīng)用程序中的內(nèi)存泄漏不會導(dǎo)致嚴(yán)重后果。
在以下情況,內(nèi)存泄漏導(dǎo)致較嚴(yán)重的后果:
- 程序運(yùn)行后置之不理,并且隨著時(shí)間的流失消耗越來越多的內(nèi)存(比如服務(wù)器上的后臺任務(wù),尤其是嵌入式系統(tǒng)中的后臺任務(wù),這些任務(wù)可能被運(yùn)行后很多年內(nèi)都置之不理);
- 新的內(nèi)存被頻繁地分配,比如當(dāng)顯示電腦游戲或動(dòng)畫視頻畫面時(shí);
- 程序能夠請求未被釋放的內(nèi)存(比如共享內(nèi)存),甚至是在程序終止的時(shí)候;
- 泄漏在操作系統(tǒng)內(nèi)部發(fā)生;
- 泄漏在系統(tǒng)關(guān)鍵驅(qū)動(dòng)中發(fā)生;
- 內(nèi)存非常有限,比如在嵌入式系統(tǒng)或便攜設(shè)備中;
- 當(dāng)運(yùn)行于一個(gè)終止時(shí)內(nèi)存并不自動(dòng)釋放的操作系統(tǒng)(比如AmigaOS)之上,而且一旦丟失只能通過重啟來恢復(fù)。
下面我們通過以下例子來介紹如何檢測內(nèi)存泄漏問題:
- #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循環(huán)調(diào)用GetMemory
- //那么問題將變得很嚴(yán)重
- //while(1){GetMemory(...);}
- return 0;
- }
實(shí)際中不可能這么簡單,如果這么簡單也用不著別的方法,程序員一眼就可以看出問題,此程序只用于測試。
2、Windows平臺下的內(nèi)存泄漏檢測
2.1、檢測是否存在內(nèi)存泄漏問題
Windows平臺下面Visual Studio 調(diào)試器和 C 運(yùn)行時(shí) (CRT) 庫為我們提供了檢測和識別內(nèi)存泄漏的有效方法,原理大致如下:內(nèi)存分配要通過CRT在運(yùn)行時(shí)實(shí)現(xiàn),只要在分配內(nèi)存和釋放內(nèi)存時(shí)分別做好記錄,程序結(jié)束時(shí)對比分配內(nèi)存和釋放內(nèi)存的記錄就可以確定是不是有內(nèi)存泄漏。在vs中啟用內(nèi)存檢測的方法如下:
- STEP1,在程序中包括以下語句: (#include 語句必須采用上文所示順序。 如果更改了順序,所使用的函數(shù)可能無法正常工作。)
- #define _CRTDBG_MAP_ALLOC
- #include <stdlib.h>
- #include <crtdbg.h>
通過包括 crtdbg.h,將 malloc 和 free 函數(shù)映射到它們的調(diào)試版本,即 _malloc_dbg 和 _free_dbg,這兩個(gè)函數(shù)將跟蹤內(nèi)存分配和釋放。 此映射只在調(diào)試版本(在其中定義了_DEBUG)中發(fā)生。 發(fā)布版本使用普通的 malloc 和 free 函數(shù)。
#define 語句將 CRT 堆函數(shù)的基版本映射到對應(yīng)的“Debug”版本。 并非絕對需要該語句;但如果沒有該語句,內(nèi)存泄漏轉(zhuǎn)儲包含的有用信息將較少。
- STEP2, 在添加了上述語句之后,可以通過在程序中包括以下語句(通常應(yīng)恰好放在程序退出位置之前)來轉(zhuǎn)儲內(nèi)存泄漏信息:
- _CrtDumpMemoryLeaks();
此時(shí),完整的代碼如下:
- #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;
- }
當(dāng)在調(diào)試器下運(yùn)行程序時(shí),_CrtDumpMemoryLeaks 將在“輸出”窗口中顯示內(nèi)存泄漏信息。 內(nèi)存泄漏信息如下所示:
如果沒有使用 #define _CRTDBG_MAP_ALLOC 語句,內(nèi)存泄漏轉(zhuǎn)儲將如下所示:
未定義 _CRTDBG_MAP_ALLOC 時(shí),所顯示的會是:
- 內(nèi)存分配編號(在大括號內(nèi))。
- 塊類型(普通、客戶端或 CRT)。
- “普通塊”是由程序分配的普通內(nèi)存。
- “客戶端塊”是由 MFC 程序用于需要析構(gòu)函數(shù)的對象的特殊類型內(nèi)存塊。 MFC new 操作根據(jù)正在創(chuàng)建的對象的需要?jiǎng)?chuàng)建普通塊或客戶端塊。
- “CRT 塊”是由 CRT 庫為自己使用而分配的內(nèi)存塊。 CRT 庫處理這些塊的釋放,因此您不大可能在內(nèi)存泄漏報(bào)告中看到這些塊,除非出現(xiàn)嚴(yán)重錯(cuò)誤(例如 CRT 庫損壞)。
從不會在內(nèi)存泄漏信息中看到下面兩種塊類型:
- “可用塊”是已釋放的內(nèi)存塊。
- “忽略塊”是您已特別標(biāo)記的塊,因而不出現(xiàn)在內(nèi)存泄漏報(bào)告中。
- 十六進(jìn)制形式的內(nèi)存位置。
- 以字節(jié)為單位的塊大小。
- 前 16 字節(jié)的內(nèi)容(亦為十六進(jìn)制)。
定義了 _CRTDBG_MAP_ALLOC 時(shí),還會顯示在其中分配泄漏的內(nèi)存的文件。 文件名后括號中的數(shù)字(本示例中為 10)是該文件中的行號。
注意:如果程序總是在同一位置退出,調(diào)用 _CrtDumpMemoryLeaks 將非常容易。 如果程序從多個(gè)位置退出,則無需在每個(gè)可能退出的位置放置對 _CrtDumpMemoryLeaks 的調(diào)用,而可以在程序開始處包含以下調(diào)用:
- _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
該語句在程序退出時(shí)自動(dòng)調(diào)用 _CrtDumpMemoryLeaks。 必須同時(shí)設(shè)置 _CRTDBG_ALLOC_MEM_DF 和_CRTDBG_LEAK_CHECK_DF 兩個(gè)位域,如前面所示。
2.2、定位具體的內(nèi)存泄漏地方
通過上面的方法,我們幾乎可以定位到是哪個(gè)地方調(diào)用內(nèi)存分配函數(shù)malloc和new等,如上例中的GetMemory函數(shù)中,即第10行!但是不能定位到,在哪個(gè)地方調(diào)用GetMemory()導(dǎo)致的內(nèi)存泄漏,而且在大型項(xiàng)目中可能有很多處調(diào)用GetMemory。如何要定位到在哪個(gè)地方調(diào)用GetMemory導(dǎo)致的內(nèi)存泄漏?
定位內(nèi)存泄漏的另一種技術(shù)涉及在關(guān)鍵點(diǎn)對應(yīng)用程序的內(nèi)存狀態(tài)拍快照。 CRT 庫提供一種結(jié)構(gòu)類型 _CrtMemState,您可用它存儲內(nèi)存狀態(tài)的快照:
- _CrtMemState s1, s2, s3;
若要在給定點(diǎn)對內(nèi)存狀態(tài)拍快照,請向 _CrtMemCheckpoint 函數(shù)傳遞 _CrtMemState 結(jié)構(gòu)。 該函數(shù)用當(dāng)前內(nèi)存狀態(tài)的快照填充此結(jié)構(gòu):
- _CrtMemCheckpoint( &s1 );
通過向 _CrtMemDumpStatistics 函數(shù)傳遞 _CrtMemState 結(jié)構(gòu),可以在任意點(diǎn)轉(zhuǎn)儲該結(jié)構(gòu)的內(nèi)容:
- _CrtMemDumpStatistics( &s1 );
若要確定代碼中某一部分是否發(fā)生了內(nèi)存泄漏,可以在該部分之前和之后對內(nèi)存狀態(tài)拍快照,然后使用 _CrtMemDifference 比較這兩個(gè)狀態(tài):
- _CrtMemCheckpoint( &s1 );
- // memory allocations take place here
- _CrtMemCheckpoint( &s2 );
- if ( _CrtMemDifference( &s3, &s1, &s2) )
- _CrtMemDumpStatistics( &s3 );
顧名思義,_CrtMemDifference 比較兩個(gè)內(nèi)存狀態(tài)(s1 和 s2),生成這兩個(gè)狀態(tài)之間差異的結(jié)果(s3)。 在程序的開始和結(jié)尾放置 _CrtMemCheckpoint 調(diào)用,并使用_CrtMemDifference 比較結(jié)果,是檢查內(nèi)存泄漏的另一種方法。 如果檢測到泄漏,則可以使用 _CrtMemCheckpoint 調(diào)用通過二進(jìn)制搜索技術(shù)來劃分程序和定位泄漏。
如上面的例子程序我們可以這樣來定位確切的調(diào)用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;
- }
調(diào)試時(shí),程序輸出如下結(jié)果:
這說明在s1和s2之間存在內(nèi)存泄漏!!!如果GetMemory不是在s1和s2之間調(diào)用,那么就不會有信息輸出。
3、Linux平臺下的內(nèi)存泄漏檢測
在上面我們介紹了,vs中在代碼中“包含crtdbg.h,將 malloc 和 free 函數(shù)映射到它們的調(diào)試版本,即 _malloc_dbg 和 _free_dbg,這兩個(gè)函數(shù)將跟蹤內(nèi)存分配和釋放。 此映射只在調(diào)試版本(在其中定義了_DEBUG)中發(fā)生。 發(fā)布版本使用普通的 malloc 和 free 函數(shù)。”即為malloc和free做了鉤子,用于記錄內(nèi)存分配信息。
Linux下面也有原理相同的方法——mtrace,http://en.wikipedia.org/wiki/Mtrace。方法類似,我這就不具體描述,參加給出的鏈接。這節(jié)我主要介紹一個(gè)非常強(qiáng)大的工具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中調(diào)用了GetMemory導(dǎo)致的內(nèi)存泄漏,GetMemory中是調(diào)用了malloc導(dǎo)致泄漏了100字節(jié)的內(nèi)存。
- 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、總結(jié)
其實(shí)內(nèi)存泄漏的原因可以概括為:調(diào)用了malloc/new等內(nèi)存申請的操作,但缺少了對應(yīng)的free/delete,總之就是,malloc/new比free/delete的數(shù)量多。我們在編程時(shí)需要注意這點(diǎn),保證每個(gè)malloc都有對應(yīng)的free,每個(gè)new都有對應(yīng)的deleted!!!平時(shí)要養(yǎng)成這樣一個(gè)好的習(xí)慣。
要避免內(nèi)存泄漏可以總結(jié)為以下幾點(diǎn):
程序員要養(yǎng)成良好習(xí)慣,保證malloc/new和free/delete匹配;
檢測內(nèi)存泄漏的關(guān)鍵原理就是,檢查malloc/new和free/delete是否匹配,一些工具也就是這個(gè)原理。要做到這點(diǎn),就是利用宏或者鉤子,在用戶程序與運(yùn)行庫之間加了一層,用于記錄內(nèi)存分配情況。