内存泄漏

内存泄漏(英語: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.

外部連結