C/C++記憶體洩漏及檢測

發表於2015-11-30

“該死系統存在記憶體洩漏問題”,專案中由於各方面因素,總是有人抱怨存在記憶體洩漏,系統長時間執行之後,可用記憶體越來越少,甚至導致了某些服務失敗。記憶體洩漏是最難發現的常見錯誤之一,因為除非用完記憶體或呼叫malloc失敗,否則都不會導致任何問題。實際上,使用C/C++這類沒有垃圾回收機制的語言時,你很多時間都花在處理如何正確釋放記憶體上。如果程式執行時間足夠長,如後臺程式執行在伺服器上,只要伺服器不當機就一直執行,一個小小的失誤也會對程式造成重大的影響,如造成某些關鍵服務失敗。

對於記憶體洩漏,本人深有體會!實習的時候,公司一個專案中就存在記憶體洩漏問題,專案的程式碼兩非常大,後臺程式也比較多,造成記憶體洩漏的地方比較難找。這次機會是我對如何查詢記憶體洩漏問題,有了一定的經驗,後面自己的做了相關實驗,在此我分享一下記憶體洩漏如何除錯查詢,主要內容如下:

  • 1、記憶體洩漏簡介
  • 2、Windows平臺下的記憶體洩漏檢測
    • 2.1、檢測是否存在記憶體洩漏問題
    • 2.2、定位具體的記憶體洩漏地方
  • 3、Linux平臺下的記憶體洩漏檢測
  • 4、總結

其實Windows、Linux下面的記憶體檢測都可以單獨開篇詳細介紹,方法和工具也遠遠不止文中介紹到的,我的方法也不是最優的,如果您有更好的方法,也請您告訴我和大家。

1、記憶體洩漏簡介及後果

wikipedia中這樣定義記憶體洩漏:在電腦科學中,記憶體洩漏指由於疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況。記憶體洩漏並非指記憶體在物理上的消失,而是應用程式分配某段記憶體後,由於設計錯誤,導致在釋放該段記憶體之前就失去了對該段記憶體的控制,從而造成了記憶體的浪費。

最難捉摸也最難檢測到的錯誤之一是記憶體洩漏,即未能正確釋放以前分配的記憶體的 bug。 只發生一次的小的記憶體洩漏可能不會被注意,但洩漏大量記憶體的程式或洩漏日益增多的程式可能會表現出各種徵兆:從效能不良(並且逐漸降低)到記憶體完全用盡。 更糟的是,洩漏的程式可能會用掉太多記憶體,以致另一個程式失敗,而使使用者無從查詢問題的真正根源。 此外,即使無害的記憶體洩漏也可能是其他問題的徵兆。

記憶體洩漏會因為減少可用記憶體的數量從而降低計算機的效能。最終,在最糟糕的情況下,過多的可用記憶體被分配掉導致全部或部分裝置停止正常工作,或者應用程式崩潰。記憶體洩漏可能不嚴重,甚至能夠被常規的手段檢測出來。在現代作業系統中,一個應用程式使用的常規記憶體在程式終止時被釋放。這表示一個短暫執行的應用程式中的記憶體洩漏不會導致嚴重後果。

在以下情況,記憶體洩漏導致較嚴重的後果:

  • 程式執行後置之不理,並且隨著時間的流失消耗越來越多的記憶體(比如伺服器上的後臺任務,尤其是嵌入式系統中的後臺任務,這些任務可能被執行後很多年內都置之不理);
  • 新的記憶體被頻繁地分配,比如當顯示電腦遊戲或動畫視訊畫面時;
  • 程式能夠請求未被釋放的記憶體(比如共享記憶體),甚至是在程式終止的時候;
  • 洩漏在作業系統內部發生;
  • 洩漏在系統關鍵驅動中發生;
  • 記憶體非常有限,比如在嵌入式系統或便攜裝置中;
  • 當執行於一個終止時記憶體並不自動釋放的作業系統(比如AmigaOS)之上,而且一旦丟失只能通過重啟來恢復。

下面我們通過以下例子來介紹如何檢測記憶體洩漏問題:

實際中不可能這麼簡單,如果這麼簡單也用不著別的方法,程式設計師一眼就可以看出問題,此程式只用於測試。

2、Windows平臺下的記憶體洩漏檢測

2.1、檢測是否存在記憶體洩漏問題

Windows平臺下面Visual Studio 偵錯程式和 C 執行時 (CRT) 庫為我們提供了檢測和識別記憶體洩漏的有效方法,原理大致如下:記憶體分配要通過CRT在執行時實現,只要在分配記憶體和釋放記憶體時分別做好記錄,程式結束時對比分配記憶體和釋放記憶體的記錄就可以確定是不是有記憶體洩漏。在vs中啟用記憶體檢測的方法如下:

  • STEP1,在程式中包括以下語句: (#include 語句必須採用上文所示順序。 如果更改了順序,所使用的函式可能無法正常工作。)

通過包括 crtdbg.h,將 malloc 和 free 函式對映到它們的除錯版本,即 _malloc_dbg 和 _free_dbg,這兩個函式將跟蹤記憶體分配和釋放。 此對映只在除錯版本(在其中定義了_DEBUG)中發生。 釋出版本使用普通的 malloc 和 free 函式。

#define 語句將 CRT 堆函式的基版本對映到對應的“Debug”版本。 並非絕對需要該語句;但如果沒有該語句,記憶體洩漏轉儲包含的有用資訊將較少。

  • STEP2, 在新增了上述語句之後,可以通過在程式中包括以下語句(通常應恰好放在程式退出位置之前)來轉儲記憶體洩漏資訊:

此時,完整的程式碼如下:

當在偵錯程式下執行程式時,_CrtDumpMemoryLeaks 將在“輸出”視窗中顯示記憶體洩漏資訊。 記憶體洩漏資訊如下所示:

20110220175032977

如果沒有使用 #define _CRTDBG_MAP_ALLOC 語句,記憶體洩漏轉儲將如下所示:

201102201750332863

 

 

 

未定義 _CRTDBG_MAP_ALLOC 時,所顯示的會是:

  • 記憶體分配編號(在大括號內)。
  • 塊型別(普通、客戶端或 CRT)。
  • “普通塊”是由程式分配的普通記憶體。
  • “客戶端塊”是由 MFC 程式用於需要解構函式的物件的特殊型別記憶體塊。 MFC new 操作根據正在建立的物件的需要建立普通塊或客戶端塊。
  • “CRT 塊”是由 CRT 庫為自己使用而分配的記憶體塊。 CRT 庫處理這些塊的釋放,因此您不大可能在記憶體洩漏報告中看到這些塊,除非出現嚴重錯誤(例如 CRT 庫損壞)。

從不會在記憶體洩漏資訊中看到下面兩種塊型別:

  • “可用塊”是已釋放的記憶體塊。
  • “忽略塊”是您已特別標記的塊,因而不出現在記憶體洩漏報告中。
  • 十六進位制形式的記憶體位置。
  • 以位元組為單位的塊大小。
  • 前 16 位元組的內容(亦為十六進位制)。

定義了 _CRTDBG_MAP_ALLOC 時,還會顯示在其中分配洩漏的記憶體的檔案。 檔名後括號中的數字(本示例中為 10)是該檔案中的行號。

注意:如果程式總是在同一位置退出,呼叫 _CrtDumpMemoryLeaks 將非常容易。 如果程式從多個位置退出,則無需在每個可能退出的位置放置對_CrtDumpMemoryLeaks 的呼叫,而可以在程式開始處包含以下呼叫:

該語句在程式退出時自動呼叫 _CrtDumpMemoryLeaks。 必須同時設定 _CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF兩個位域,如前面所示。

2.2、定位具體的記憶體洩漏地方

通過上面的方法,我們幾乎可以定位到是哪個地方呼叫記憶體分配函式malloc和new等,如上例中的GetMemory函式中,即第10行!但是不能定位到,在哪個地方呼叫GetMemory()導致的記憶體洩漏,而且在大型專案中可能有很多處呼叫GetMemory。如何要定位到在哪個地方呼叫GetMemory導致的記憶體洩漏?

定位記憶體洩漏的另一種技術涉及在關鍵點對應用程式的記憶體狀態拍快照。 CRT 庫提供一種結構型別 _CrtMemState,您可用它儲存記憶體狀態的快照:

若要在給定點對記憶體狀態拍快照,請向 _CrtMemCheckpoint 函式傳遞 _CrtMemState 結構。 該函式用當前記憶體狀態的快照填充此結構:

通過向 _CrtMemDumpStatistics 函式傳遞 _CrtMemState 結構,可以在任意點轉儲該結構的內容:

若要確定程式碼中某一部分是否發生了記憶體洩漏,可以在該部分之前和之後對記憶體狀態拍快照,然後使用 _CrtMemDifference 比較這兩個狀態:

顧名思義,_CrtMemDifference 比較兩個記憶體狀態(s1 和 s2),生成這兩個狀態之間差異的結果(s3)。 在程式的開始和結尾放置 _CrtMemCheckpoint 呼叫,並使用_CrtMemDifference 比較結果,是檢查記憶體洩漏的另一種方法。 如果檢測到洩漏,則可以使用 _CrtMemCheckpoint 呼叫通過二進位制搜尋技術來劃分程式和定位洩漏。

如上面的例子程式我們可以這樣來定位確切的呼叫GetMemory的地方:

除錯時,程式輸出如下結果:

201102201750331958

這說明在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。

如下圖所示:

201102201750364202

如上圖所示知道:

==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是否匹配,一些工具也就是這個原理。要做到這點,就是利用巨集或者鉤子,在使用者程式與執行庫之間加了一層,用於記錄記憶體分配情況。

相關文章