記憶體洩露

Mr_John_Liang發表於2013-05-21
程式設計時進行動態記憶體分配是非常必要的。它可以在程式執行的過程中幫助分配所需的記憶體,而不是在程式啟動的時候就進行分配。然而,有效地管理這些記憶體同樣也是非常重要的。在大型的、複雜的應用程式中,記憶體洩漏是常見的問題。當以前分配的一片記憶體不再需要使用或無法訪問時,但是卻並沒有釋放它,那麼對於該程式來說,會因此導致總可用記憶體的減少,這時就出現了記憶體洩漏。儘管優秀的程式設計實踐可以確保最少的洩漏,但是根據經驗,當使用大量的函式對相同的記憶體塊進行處理時,很可能會出現記憶體洩漏。尤其是在碰到錯誤路徑的情況下更是如此。

編輯本段定義

一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小可以在程式執行期決定),使用完後必須顯式釋放的記憶體。應用程式一般使用malloc,realloc,new等函式從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。以下這段小程式演示了堆記憶體發生洩漏的情形:
void MyFunction(int nSize)
{
char* p= new char[nSize];
if( !GetStringFrom( p, nSize ) ){
MessageBox(“Error”);
return;
}
…//using the string pointed by p;
delete[] p;
}
當函式GetStringFrom()返回零的時候,指標p指向的記憶體就不會被釋放。這是一種常見的發生記憶體洩漏的情形。程式在入口處分配記憶體,在出口處釋放記憶體,但是c函式可以在任何地方退出,所以一旦有某個出口處沒有釋放應該釋放的記憶體,就會發生記憶體洩漏。

編輯本段簡介

在電腦科學中,記憶體洩漏(memory leak)指由於疏忽或錯誤使程 序未能釋放而造成不能再使用的記憶體的情況。記憶體洩漏並非指記憶體在物理上的消失,而是應用程式分配某段記憶體後,由於設計錯誤,失去了對該段記憶體的控制,因而造成了記憶體的浪費。記憶體洩漏與許多其他問題有著相似的症狀,並且通常情況下只能由那些可以獲得程式原始碼程式設計師才可以分析出來。然而,有不少人習慣於把任何不需要的記憶體使用的增加描述為記憶體洩漏,嚴格意義上來說這是不準確的。
一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小可以在程式執行期決定),使用完後必須顯式釋放的記憶體。應用程式一般使用malloc,calloc,realloc等函式(C++中使用new操作符)從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。

編輯本段分類

1. 常發性記憶體洩漏。發生記憶體洩漏的程式碼會被多次執行到,每次被執行的時候都會導致一塊記憶體洩漏。
2. 偶發性記憶體洩漏。發生記憶體洩漏的程式碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測記憶體洩漏至關重要。
3. 一次性記憶體洩漏。發生記憶體洩漏的程式碼只會被執行一次,或者由於演算法上的缺陷,導致總會有一塊且僅一塊記憶體發生洩漏。比如,在一個Singleton類的建構函式中分配記憶體,在解構函式中卻沒有釋放該記憶體。而Singleton類只存在一個例項,所以記憶體洩漏只會發生一次。
4. 隱式記憶體洩漏。程式在執行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要執行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所以,我們稱這類記憶體洩漏為隱式記憶體洩漏。

編輯本段程式設計

記憶體洩漏是程式設計中一項常見錯誤,特別是使用沒有內建自動垃圾回收程式語言,如CC++。一般情況下,記憶體洩漏發生是因為不能存取動態分配的記憶體。目前有相當數量的除錯工具用於檢測不能存取的記憶體,從而可以防止記憶體洩漏問題,如IBM Rational Purify、BoundsChecker、Valgrind、Insure++及memwatch都是為C/C++程式設計亦較受歡迎的記憶體除錯工具。飛鴿傳書垃圾回收則可以應用到任何程式語言,而C/C++也有此類函式庫。
提供自動記憶體管理程式語言JavaVB.NET.Net記憶體洩露)以及LISP,都不能避免記憶體洩漏。例如,程式會把專案加入至列表,但在完成時沒有移除,如同人把物件丟到一堆物品中或放到抽屜內,但後來忘記取走這件物品一樣。記憶體管理器不能判斷專案是否將再被存取,除非程式作出一些指示表明不會再被存取。
雖然記憶體管理器可以回覆不能存取的記憶體,但它不可以釋放可存取的記憶體因為仍有可能需要使用。現代的記憶體管理器因此為程式設計員提供技術來標示記憶體的可用性,以不同級別的“存取性”表示。記憶體管理器不會把需要存取可能較高的物件釋放。當物件直接和一個強引用相關或者間接和一組強引用相關表示該物件存取性較強。(強引用相對於弱引用,是防止物件被回收的一個引用。)要防止此類記憶體洩漏,開發者必須使用物件後清理引用,一般都是在不再需要時將引用設成null,如果有可能,把維持強引用的事件偵聽器全部登出。
一般來說,自動記憶體管理對開發者來講比較方便,因為他們不需要實現釋放的動作,或擔心清理記憶體的順序,而不用考慮物件是否依然被引用。對開發者來說,瞭解一個引用是否有必要保持比了解一個物件是否被引用要簡單得多。但是,自動記憶體管理不能消除所有的內容洩漏。

編輯本段影響

如果一個程式存在記憶體洩漏並且它的記憶體使用量穩定增長,通常不會有很快的症狀。每個物理系統都有一個較大的記憶體量,如果記憶體洩漏沒有被中止(比如重啟造成洩漏的程式)的話,它遲早會造成問題。
大多數的現代計算機作業系統都有儲存在RAM晶片中主記憶體和儲存在次級儲存裝置如硬碟中的虛擬記憶體記憶體分配是動態的——每個程式根據要求獲得相應的記憶體。存取活躍的頁面檔案被轉移到主記憶體以提高存取速度;反之,存取不活躍的頁面檔案被轉移到次級儲存裝置。當一個簡單的程式消耗大量的記憶體時,它通常佔用越來越多的主記憶體,使其他程式轉到次級儲存裝置,使系統的執行效率大大降低。甚至在有記憶體洩漏的程式終止後,其他程式需要相當長的時間才能切換到主記憶體,恢復原來的執行效率。
當系統所有的記憶體全部耗完後(包括主記憶體和虛擬記憶體,在嵌入式系統中,僅有主記憶體),所有申請記憶體的操作將失敗。這通常導致程式試圖申請記憶體來終止自己,或造成分段記憶體訪問錯誤(segmentation fault)。現在有一些專門為修復這種情況而設計的程式,常用的辦法是預留一些記憶體。值得注意的是,第一個遭遇得不到記憶體問題的程式有時候並不是有記憶體洩漏的程式。
一些多工作業系統有特殊的機制來處理記憶體耗盡得情況,如隨機終止一個程式(可能會終止一些正常的程式),或終止耗用記憶體最大的程式(很有可能是引起記憶體洩漏的程式)。另一些作業系統則有記憶體分配限制,這樣可以防止任何一個程式耗用完整個系統的記憶體。這種設計的缺點是有時候某些程式確實需要較大數量的記憶體時,如一些處理影像,視訊和科學計算的程式,作業系統需要重新配置。
記憶體洩漏發生在核心,表示作業系統自身發生了問題。那些沒有完善的記憶體管理的計算機,如嵌入式系統,會因為一個長時間的記憶體洩漏而崩潰。
一些被公眾訪問的系統,如網路伺服器路由器很容易被黑客攻擊,加入一段攻擊程式碼,而產生記憶體洩漏。

編輯本段記憶體消耗

值得注意的是,記憶體用量持續增加不一定表明記憶體洩漏。一些應用程式會儲存越來越多資料到記憶體中(如用作快取。如果快取太大引起問題,這可能是程式設計上的錯誤,但並非是記憶體洩漏因為資料仍被使用。另一方面,程式有可能申請不合理的大量記憶體因為程式設計者假設記憶體總是足夠執行特定的工作;例如,影像檔案處理器會在開始時閱讀影像檔案的內容並把之儲存至記憶體中,有時候由於影像檔案太大,消耗的記憶體超過了可用的記憶體導致失敗。
另一角度講,記憶體洩漏是一種特殊的程式設計錯誤,如果沒有原始碼,根據徵兆只能猜測可能有記憶體洩漏。在這種情況下,使用術語“記憶體消耗持續增加”可能更確切。

編輯本段檢測

檢測記憶體洩漏的關鍵是要能截獲住對分配記憶體和釋放記憶體的函式的呼叫。截獲住這兩個函式,我們就能跟蹤每一塊記憶體的生命週期,比如,每當成功的分配一塊記憶體後,就把它的指標加入一個全域性的list中;每當釋放一塊記憶體,再把它的指標從list中刪除。這樣,當程式結束的時候,list中剩餘的指標就是指向那些沒有被釋放的記憶體。這裡只是簡單的描述了檢測記憶體洩漏的基本原理,詳細的演算法可以參見Steve Maguire的<<Writing Solid Code>>。
如果要檢測堆記憶體的洩漏,那麼需要截獲住malloc/realloc/free和new/delete就可以了(其實new/delete最終也是用malloc/free的,所以只要截獲前面一組即可)。對於其他的洩漏,可以採用類似的方法,截獲住相應的分配和釋放函式。比如,要檢測BSTR的洩漏,就需要截獲SysAllocString/SysFreeString;要檢測HMENU的洩漏,就需要截獲CreateMenu/ DestroyMenu。(有的資源的分配函式有多個,釋放函式只有一個,比如,SysAllocStringLen也可以用來分配BSTR,這時就需要截獲多個分配函式)
在Windows平臺下,檢測記憶體洩漏的工具常用的一般有三種,MS C-Runtime Library內建的檢測功能;外掛式的檢測工具,諸如,Purify,BoundsChecker等;利用Windows NT自帶的Performance Monitor。這三種工具各有優缺點,MS C-Runtime Library雖然功能上較之外掛式的工具要弱,但是它是免費的;Performance Monitor雖然無法標示出發生問題的程式碼,但是它能檢測出隱式的記憶體洩漏的存在,這是其他兩類工具無能為力的地方。

編輯本段後果

記憶體洩漏會因為減少可用記憶體的數量從而降低計算機的效能。最終,在最糟糕的情況下,過多的可用記憶體被分配掉導致全部或部分裝置停止正常工作,或者應用程式崩潰。
記憶體洩漏可能不嚴重,甚至能夠被常規的手段檢測出來。在現代作業系統中,一個應用程式使用的常規記憶體在程式終止時被釋放。這表示一個短暫執行的應用程式中的記憶體洩漏不會導致嚴重後果。
在以下情況,記憶體洩漏導致較嚴重的後果:
* 程式執行後置之不理,並且隨著時間的流失消耗越來越多的記憶體(比如伺服器上的後臺任務,尤其是嵌入式系統中的後臺任務,這些任務可能被執行後很多年內都置之不理)
* 新的記憶體被頻繁地分配,比如當顯示電腦遊戲或動畫視訊畫面時
* 程式能夠請求未被釋放的記憶體(比如共享記憶體),甚至是在程式終止的時候
* 洩漏在作業系統內部發生
* 洩漏在系統關鍵驅動中發生
* 記憶體非常有限,比如在嵌入式系統或便攜裝置中
* 當執行於一個終止時記憶體並不自動釋放的作業系統(比如AmigaOS)之上,而且一旦丟失只能通過重啟來恢復。

編輯本段幾種原因

1、對於通過new等運算子申請到的記憶體空間在使用之後沒有釋放掉。關於這個問題,如果是在過程程式中開闢的空間,可以在過程結束時釋放;但是如果是物件導向的程式設計,在類的建構函式中開闢的空間,那麼記得一定要在解構函式中釋放,但是如果解構函式出現問題了,導致不能釋放記憶體空間,就造成了記憶體洩露。
2、對於程式中的windows控制程式碼使用完要close掉。
3、對於記憶體的洩露有的時候是忘記了回收,但是有的時候是無法回收,比如1中提到的解構函式不正確導致記憶體洩露,這是屬於程式有問題;還有關於物件導向程式設計的一個記憶體洩露的可能性:一個物件在建構函式丟擲異常,物件本身的記憶體會被成功釋放,但是其解構函式不會被呼叫,其內部成員變數都可以成功析構,但是使用者在建構函式中動態生成的物件無法成功釋放。如果一個物件在建構函式中開啟很多系統資源,但是建構函式中後續程式碼丟擲了異常,則這些資源將不會被釋放,建議在建構函式中加入try catch語句,對先前申請的資源進行釋放後(也就是做解構函式該做的事情)再次丟擲異常,確保記憶體和其他資源被成功回收。也就是說建構函式出現問題會導致建構函式中開闢的記憶體空間不能回收,對於物件本身的記憶體空間還是可以回收的。

編輯本段例子

#include<malloc.h>
#define LEN sizeof(struct student)
#define NULL 0
struct student
{
long num;
float score;
struct student *next;
};
int n;
struct student*creat(void)
{
struct student *head;
struct student *p1,*p2;
n=0;
p1=p2=(struct student*)malloc(LEN);
scanf("%ld,%f",&p1->num,&p1->score);
head=NULL; //這句可以不要麼?不可以!不要這句話,就可能造成記憶體洩露
while(p1->num!=0)
{
n=n+1;
if(n==1)head=p1;
else p2->next=p1;
p2=p1;
p1=(struct student*)malloc(LEN);
scanf("%ld,%f",&p1->num,&p1->score);
}
p2->next=NULL;
return(head);
}

編輯本段常見問題

以下例子無需任何程式設計上的知識,但能表明如何導致記憶體洩漏及其造成的影響。注意以下的例子是虛構的。
在此例中的應用程式是一個簡單軟體的一小部分,FreeEIM用來控制升降機的運作。此部分軟體當乘客在升降機內按下一樓層的按鈕時執行。
當按下按鈕時:
1.得到記憶體,用作記住目的樓層
2.把目的樓層的數字儲存到記憶體
3.升降機是否已到達目的樓層?
如是,沒有任何事需要做:程式完成
否則:
(1).等待直至升降機停止
(2).到達指定樓層
(3).把剛才用作記住目的樓層的記憶體釋出。
此程式有一處會造成記憶體洩漏。如果在升降機所在樓層按下該層的按鈕,記憶體就會一直被佔用而不再釋放。這種情況發生得越多,洩漏的記憶體則越多。
這個小錯誤不會造成即時影響。因為人不會經常在升降機所在樓層按下同一層的按鈕。而且在通常情況下,升降機應有足夠的記憶體以應付上百次、上千次類似的情況。不過,升降機最後仍有可能消耗完所有記憶體。這可能需要數個月或是數年,所以在簡單的測試下這個問題不會被發現。
而這個例子導致的後果會是不那麼令人愉快。至少,升降機不會再響應前往其他樓層的要求。更嚴重的是,如果程式需要記憶體去開啟升降機門,那可能有人被困升降機內,因為升降機沒有足夠的記憶體去開啟升降機門。
記憶體洩漏只會在程式執行的時間內持續。例如:關閉升降機的電源時,程式終止執行。當電源再度開啟,程式會再次執行而記憶體會重置,而這種緩慢的洩漏亦會從頭開始再次發生。

相關文章