記憶體洩漏

記憶體洩漏（memory leak）是電腦科學中的一種資源洩漏，主因是電腦程序的記憶體管理失當，因而失去對一段已分配記憶體空間的控制，程序繼續占用已不再使用的記憶體空間，或是記憶體所儲存之物件無法透過執行程式碼而存取，令記憶體資源空耗。

記憶體漏失與許多其他問題情形具有相同徵兆，通常只有獲得程序原始碼的程式設計師能分析診斷是否發生記憶體漏失。

## 後果

記憶體洩漏會因為減少可用記憶體的數量從而降低電腦的性能。最終，在最糟糕的情況下，過多的可用記憶體被分配掉導致全部或部分設備停止正常工作，或者應用程式崩潰。

記憶體洩漏帶來的後果可能是不嚴重的，有時甚至能夠被常規的手段檢測出來。在現代作業系統中，一個應用程式使用的常規內存在程序終止時被釋放。這表示一個短暫運行的應用程式中的記憶體洩漏不會導致嚴重後果。

在以下情況，記憶體洩漏後果較嚴重：

- 程序運行後置之不理，並且隨著時間的流逝消耗越來越多的記憶體（比如伺服器上的後台任務，尤其是嵌入式系統中的後台任務，這些任務可能被運行後很多年內都置之不理）。
- 新的記憶體被頻繁地分配，比如當顯示電腦遊戲或動畫影片畫面時。
- 程序能夠請求即使在程序終止之後也不會被釋放的記憶體（比如共享內存）。
- 洩漏在作業系統內部發生。
- 洩漏在系統關鍵驅動中發生。
- 記憶體非常有限，比如在嵌入式系統或便攜設備中。
- 當運行於一個程序終止時記憶體並不自動釋放記憶體的作業系統（比如AmigaOS）之上時。

## 簡例

以下的虛構例子無需任何程式設計的知識，但能表明如何導致記憶體洩漏及其造成的影響。

在此例中的應用程式是一個簡單軟體的一小部分，用來控制升降機的運作。此部分軟體當乘客在升降機內按下一樓層的按鈕時運行。

當按下按鈕時：

- 要求使用記憶體，用作記住目的樓層
- 把目的樓層的數字儲存到記憶體中
- 升降機是否已到達目的樓層？
- 如是，沒有任何事需要做：程式完成
- 等待直至升降機停止
- 到達指定樓層
- 釋放剛才用作記住目的樓層的記憶體

此程式有一處會造成記憶體洩漏：如果在升降機所在樓層按下該層的按鈕（即上述程序的第4步），程序將觸發判斷條件而結束運行，但記憶體仍一直被占用而沒有被釋放。這種情況發生得越多，洩漏的記憶體也越多。

這個小錯誤不會造成即時影響。因為人不會經常在升降機所在樓層按下同一層的按鈕。而且在通常情況下，升降機應有足夠的記憶體以應付上百次、上千次類似的情況。不過，升降機最後仍有可能消耗完所有記憶體。這可能需要數個月或是數年，所以在簡單的測試下這個問題不會被發現。

而這個例子導致的後果會是不那麼令人愉快。至少，升降機不會再理會前往其他樓層的要求。更嚴重的是，如果程式需要記憶體去開啟升降機門，那可能有人被困升降機內，因為升降機沒有足夠的記憶體去開啟升降機門。

記憶體洩漏只會在程式運行的時間內持續。例如：關閉升降機的電源時，程式終止運行。當電源再度開啟，程式會再次運行而記憶體會重置，而這種緩慢的洩漏則會從頭開始再次發生。

## 程式設計問題

記憶體洩漏是程式設計中一項常見錯誤，特別是使用沒有內置自動垃圾回收的程式語言，如C及C++。一般情況下，記憶體洩漏發生是因為不能存取動態分配的記憶體。目前有相當數量的調試工具用於檢測不能存取的記憶體，從而可以防止記憶體洩漏問題，如IBM Rational Purify、BoundsChecker、Valgrind、Insure++及memwatch都是為C／C++程式設計亦較受歡迎的記憶體除錯工具。垃圾回收則可以應用到任何程式語言，而C／C++也有此類函式庫。

提供自動記憶體管理的編程語言如Java、C、C#、VB.NET以及LISP，都不能避免記憶體洩漏。例如，程式會把項目加入至列表，但在完成時沒有移除，如同人把物件丟到一堆物品中或放到抽屜內，但後來忘記取走這件物品一樣。記憶體管理器不能判斷項目是否將再被存取，除非程式作出一些指示表明不會再被存取。

譬如以C語言為例，在stdlib.h中提供了 malloc、calloc、free等函數，在使用malloc取得記憶體空間，則需在不需使用後free釋放，如未釋放，則會產生所謂memory leakage。

雖然記憶體管理器可以回復不能存取的記憶體，但它不可以釋放可存取的記憶體因為仍有可能需要使用。現代的記憶體管理器因此為程式設計員提供技術來標示記憶體的可用性，以不同級別的「存取性」表示。記憶體管理器不會把需要存取可能較高的對象釋放。當對象直接和一個強引用相關或者間接和一組強引用相關表示該對象存取性較強。（強引用相對於弱引用，是防止對象被回收的一個引用。）要防止此類記憶體洩漏，開發者必須使用對象後清理引用，一般都是在不再需要時將引用設成null，如果有可能，把維持強引用的事件偵聽器全部註銷。

一般來說，自動記憶體管理對開發者來講比較方便，因為他們不需要實現釋放的動作，或擔心清理記憶體的順序，而不用考慮對象是否依然被引用。對開發者來說，了解一個引用是否有必要保持比了解一個對象是否被引用要簡單得多。但是，自動記憶體管理不能消除所有的內容洩漏。

## 影響

如果一個程序存在記憶體洩漏並且它的記憶體使用量穩定增長，通常不會有很快的症狀。每個物理系統都有一個較大的記憶體量，如果記憶體洩漏沒有被中止（比如重啟造成洩漏的程序）的話，它遲早會造成問題。

大多數的現代電腦作業系統都有存儲在RAM晶片中主記憶體和存儲在次級存儲設備如硬碟中的虛擬內存，記憶體分配是動態的——每個進程根據要求獲得相應的記憶體。存取活躍的頁面文件被轉移到主記憶體以提高存取速度；反之，存取不活躍的頁面文件被轉移到次級存儲設備。當一個簡單的進程消耗大量的記憶體時，它通常占用越來越多的主記憶體，使其他程序轉到次級存儲設備，使系統的運行效率大大降低。甚至在有記憶體洩漏的程序終止後，其他程序需要相當長的時間才能切換到主記憶體，恢復原來的運行效率。

當系統所有的記憶體全部耗完後（包括主記憶體和虛擬內存，在嵌入式系統中，僅有主記憶體），所有申請記憶體的操作將失敗。這通常導致程序試圖申請記憶體來終止自己，或造成分段記憶體訪問錯誤（segmentation fault）。現在有一些專門為修復這種情況而設計的程序，常用的辦法是預留一些記憶體。值得注意的是，第一個遭遇得不到記憶體問題的程序有時候並不是有記憶體洩漏的程序。

一些多任務作業系統有特殊的機制來處理記憶體耗盡得情況，如隨機終止一個進程（可能會終止一些正常的進程），或終止耗用記憶體最大的進程（很有可能是引起記憶體洩漏的進程）。另一些作業系統則有記憶體分配限制，這樣可以防止任何一個進程耗用完整個系統的記憶體。這種設計的缺點是有時候某些進程確實需要較大數量的記憶體時，如一些處理圖像，影片和科學計算的進程，作業系統需要重新配置。

如記憶體洩漏發生在內核，表示作業系統自身發生了問題。那些沒有完善的記憶體管理的電腦，如嵌入式系統，會因為一個長時間的記憶體洩漏而崩潰。

一些被公眾訪問的系統，如網路伺服器或路由器很容易被黑客攻擊，加入一段攻擊代碼，而產生記憶體洩漏。

## 其他記憶體消耗

值得注意的是，記憶體用量持續增加不一定表明記憶體洩漏。一些應用程式會儲存越來越多資料到記憶體中（如用作快取。如果快取太大引起問題，這可能是程式設計上的錯誤，但並非是記憶體洩漏因為資料仍被使用。另一方面，程式有可能申請不合理的大量記憶體，因為程式設計者假設記憶體總是足夠運行特定的工作；例如，圖像檔案處理器會在開始時閱讀圖像檔案的內容並把之儲存至記憶體中，有時候由於圖像檔案太大，消耗的記憶體超過了可用的記憶體導致失敗。

另一角度講，記憶體洩漏是一種特殊的編程錯誤，如果沒有原始碼，根據徵兆只能猜測可能有記憶體洩漏。在這種情況下，使用術語「記憶體消耗持續增加」可能更確切。

## 例子

### C

下面是一個C語言的例子，在函數f中申請了記憶體卻沒有釋放，導致記憶體洩漏。當程式不停地重複調用這個有問題的函數f，申請記憶體函數malloc最後會在程式沒有更多可用記憶體可以申請時產生錯誤（函數輸出為NULL）。但是，由於函數malloc輸出的結果沒有加以出錯處理，因此程式會不停地嘗試申請記憶體，並且在系統有新的空閒記憶體時，被該程序占用。注意，malloc返回NULL的原因不一定是因為前述的沒有更多可用記憶體可以申請，也可能是邏輯地址空間耗盡，在Linux環境上測試的時候後者更容易發生。

```c
 #include <stdio.h>
 #include <stdlib.h>

 void f(void)
 {
     void* s;
     s = malloc(50); /* 申请内存空间 */
     return;  /* 内在泄漏 - 参见以下资料 */ 
     /* 
      * s 指向新分配的堆空间。
      * 当此函数返回，离开局部变量s的作用域后将无法得知s的值。
      * 分配的内存空间不能被释放。
      *
      * 如要「修复」这个问题，必须想办法释放分配的堆空间。
      * 也可以用alloca(3)代替malloc(3)。
      * （注意：alloca(3)既不是ANSI函数也不是POSIX函数）
      */
 }
 int main(void)
 {
     /* 该函数是一个死循环函数 */
     while (true) f(); /* Malloc函数迟早会由于内存泄漏而返回NULL*/
     return 0;
 }
```

### C++

以下例子中，儲存了整數123的記憶體空間不能被刪除，因為地址丟失了。這些空間已無法再使用。

```cpp
#include <iostream>
using namespace std;
int main()
{ 
   int *a = new int(123);
   cout << *a << endl;
   // We should write "delete a;" here
   a = new int(456);
   cout << *a << endl;
   delete a;
   return 0;
}
```

## 參閱

- 緩衝區溢出

## 外部連結

- 測試記憶體洩漏 (使用MFC除錯支援)
- Memory Leak Detection in C++
- Memory Leak Detection in Embedded Systems "
- Why doesn't my application release the memory? (Java FAQ)
- Fixing Memory Leaks in KDE"
- Memory Leaking