記憶體流失

記憶體流失(英語:memory leak)是電腦科學中的一種資源漏失,主因是計算機程式記憶體管理失當[1],因而失去對一段已分配主記憶體空間的控制,程式繼續占用已不再使用的主記憶體空間,或是記憶體所儲存之物件無法透過執行程式碼而存取,令主記憶體資源空耗[2]

「memory leak」的各地常用譯名
中國大陸內存泄漏
臺灣記憶體流失、記憶體漏失

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

後果

記憶體流失會因為減少可用主記憶體的數量從而降低電腦的效能。最終,在最糟糕的情況下,過多的可用主記憶體被分配掉導致全部或部分裝置停止正常工作,或者應用程式崩潰[3]

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

在以下情況,記憶體流失後果較嚴重:

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

簡例

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

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

當按下按鈕時:

  1. 要求使用記憶體,用作記住目的樓層
  2. 把目的樓層的數字儲存到記憶體中
  3. 電梯是否已到達目的樓層?
  4. 如是,沒有任何事需要做:程式完成
  5. 否則:
  1. 等待直至電梯停止
  2. 到達指定樓層
  3. 釋放剛才用作記住目的樓層的記憶體

此程式有一處會造成記憶體漏失:如果在電梯所在樓層按下該層的按鈕(即上述程式的第4步),程式將觸發判斷條件而結束執行,但記憶體仍一直被占用而沒有被釋放。這種情況發生得越多,漏失的記憶體也越多。

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

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

記憶體漏失只會在程式運行的時間內持續。例如:關閉電梯的電源時,程式終止運行。當電源再度開啟,程式會再次運行而記憶體會重設,而這種緩慢的漏失則會從頭開始再次發生。

程式設計問題

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

提供自動記憶體管理的程式語言如JavaCC#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環境上測試的時候後者更容易發生。

 #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的主記憶體空間不能被刪除,因為位址丟失了。這些空間已無法再使用。

#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;
}

參閱

參考資料

  1. ^ Crockford, Douglas. JScript Memory Leaks. [20 July 2022]. (原始內容存檔於7 December 2012). 
  2. ^ Creating a memory leak with Java. Stack Overflow. [2013-06-14]. (原始內容存檔於2019-11-29). 
  3. ^ Rudafshani, Masoomeh, and Paul A. S. Ward. "LeakSpot: Detection and Diagnosis of Memory Leaks in JavaScript Applications." Software, practice & experience 47.1 (2017): 97–123. Web.

外部連結