The Wayback Machine - https://web.archive.org/web/20221028214853/https://baike.sogou.com/kexue/d10090.htm

内存泄漏

编辑

在计算机科学中,内存泄漏是一种资源泄漏。发生这种情况时,不再需要的内存未被释放,计算机程序以错误的方式管理内存分配。[1] 当对象存储在内存中但不能被运行代码无法访问时,也可能发生内存泄漏。[2] 内存泄漏的症状类似于许多其他问题,通常它只能由能够访问程序源代码的程序员来诊断。

当计算机程序使用的内存超出所需的量度时,就会发生空间泄漏。与永远不会释放的内存泄漏不同,空间泄漏消耗的内存会释放,但其会比预期的晚。 [3]

因为内存泄漏会在应用程序运行时耗尽可用的系统内存,这种情况通常是软件老化的原因造成的。

1 结果编辑

内存泄漏会减少可用内存的数量,从而降低计算机的性能。最终,在最坏的情况下,它可能会分配太多的可用内存,并且系统或设备会全部或部分停止正常工作,应用程序会瘫痪,或者系统由于颠簸而大大变慢。

内存泄漏可能不严重,甚至无法通过常规手段检测到。在现代操作系统中,当应用程序终止时,其使用的正常内存会被释放。这意味着内存泄漏在只运行很短时间的程序中可能不会被注意到,其结果并不严重。

更严重的泄漏包括:

  • 程序长时间运行并且随着时间的推移消耗额外的内存,例如服务器上的后台任务运行,但是在可能运行多年的嵌入式设备中这种情况尤为突出。
  • 新的存储器被频繁地分配给一次性任务,例如当渲染计算机游戏或动画视频的每一帧时就会出现这种情况。
  • 程序可以请求不释放的内存,例如共享内存,即使此时程序会终止。
  • 存储器非常有限,例如在嵌入式系统或便携式设备中时。
  • 泄漏发生在操作系统或内存管理器中时。
  • 当系统设备驱动程序导致泄漏时。
  • 运行在程序终止时不会自动释放内存的操作系统上时。

1.1 内存泄漏的一个例子

下面的例子是用伪代码编写的,旨在展示内存泄漏是如何发生的以及它的影响,理解它不需要任何编程知识。本例中的程序是软件中一些非常简单的一部分,这些软件被设计用来控制电梯。每当电梯内的任何人按下楼层按钮时,程序的这一部分就会运行。

按下按钮时:
  获取一些内存,这些内存将用于记住楼层号
  将楼层号输入内存
  我们已经在目标楼层了吗?
    如果是这样,我们无事可做:完成了
    否则:
      等到电梯闲置
      去要求的楼层
      释放我们曾经记得楼层号的记忆

如果请求的楼层号与电梯所在的楼层相同,就会发生内存泄漏;此时系统会跳过释放内存的条件。每次出现这种情况都会造成更多的内存泄漏。

像这样的情况,其影响不会立马呈现。人们通常不会再次按下他们已经按下的按钮,而且无论如何,电梯可能有足够的空闲内存,但这种情况可能会发生成百上千次。电梯的内存终会耗尽。可能需要几个月或几年的时间才耗尽,而且尽管进行了彻底的测试,也可能发现不了这些问题。

其结果是令人不快的,至少,电梯会不再回应移动到另一楼层的请求(例如有人试图呼叫电梯或当有人在里面并按下楼层按钮时)。如果程序的其他部分需要内存(例如,指定用于打开和关闭门的部分),那么可能有人会被困在里面,或者如果没有人在里面,那么没有人能够使用电梯,因为软件不能打开门。

内存泄漏会一直持续到系统复位。例如:如果电梯断电或停电,程序将停止运行。当再次打开电源时,程序将重新启动,所有内存将再次可用,但是缓慢的内存泄漏过程会与程序一起重新启动,最终影响系统的正确运行。

上述示例中的泄漏可以通过将“释放”操作置于条件之外来纠正:

按下按钮时:
  获取一些内存,这些内存将用于记住楼层号
  将楼层号输入内存
  我们已经在目标楼层了吗?
    如果不是:
      等到电梯闲置
      去要求的楼层
  释放我们曾经记得楼层号的记忆

2 方案问题编辑

在使用没有内置的自动垃圾收集语言时,内存泄漏是编程中的一个常见错误,如C和C++。通常情况下,内存泄漏是因为动态分配的内存变得不可访问。内存泄漏漏洞的流行促成了许多调试工具的开发以检测不可访问的内存。BoundsChecker、Deleaker、IBM Rational Purify、Valgrind、Parasoft Insure++、Dr. Memory和memwatch是一些更流行的C和C++程序内存调试器。“保守的”垃圾收集功能可以作为内置功能添加到任何缺少它的编程语言中,并且用于这样做的库可用于C和C++程序。保守的收集器会找到并回收大部分(但不是全部)不可访问的内存。

尽管内存管理器可以恢复不可访问的内存,但它无法释放仍然可访问的内存,因此它可能仍然有用。因此,现代内存管理器为程序员提供了语义标记不同有用级别的内存的技术,这些有用级别对应于不同的可达性级别。内存管理器不会释放一个强可访问的对象。如果对象可以通过强引用直接到达或通过强引用链间接到达,则该对象是强可达的。(强引用不同于弱引用,它防止对象被垃圾收集),为了防止这种情况,开发人员需负责在使用后清理引用,通常是在不再需要时将引用设置为null,并在必要时取消注册任何保持对对象的强引用的事件侦听器。

一般来说,自动内存管理对开发人员来说更加强大和方便,因为他们不需要实现释放例程,也不需要担心执行清理的顺序,也不需要担心对象是否仍然被引用。程序员知道何时不再需要引用比知道何时不再引用对象更容易。然而,自动内存管理会带来性能开销,并且它不能消除导致内存泄漏的所有编程错误。

3 RAII编辑

RAII是资源获取即初始化的缩写,是解决C++、D和Ada中常见问题的一种方法。它包括将-*范围内的对象与获取的资源相关联,并且一旦对象超出范围就自动释放资源。与垃圾收集不同,RAII的优势在于知道对象何时存在,何时不存在。比较以下C和C++示例:

/* C version */
#include <stdlib.h>

void f(int n)
{
  int* array = calloc(n, sizeof(int));
  do_some_work(array);
  free(array);
}

// C++ version
#include <vector>

void f(int n)
{
  std::vector<int> array (n);
  do_some_work(array);
}

如示例中实现的,C版本需要显式解除分配;数组是动态分配的(在大多数C实现中是从堆中),并且一直存在,直到显式释放。

C++版本不需要显式解除分配;一旦对象数组超出范围,它总是自动发生,包括如果抛出异常。这避免了垃圾收集方案的一些开销。由于对象析构器可以释放内存以外的资源,RAII有助于防止通过句柄访问的输入和输出资源的泄漏,而句柄是标记和清除垃圾收集无法正常处理的。这些包括打开的文件、打开的窗口、用户通知、图形库中的对象、线程同步原语(如关键部分)、网络连接以及到窗口注册表或其他数据库的连接。

然而,正确使用RAII并不总是容易的,而且它有自己的陷阱。例如,如果不小心操作,通过引用返回数据来创建悬挂指针(或引用)是可能的,只有当数据的包含对象超出范围时,才删除该数据。

D使用RAII和垃圾收集的组合,当一个对象显然不能在其原始范围之外被访问时使用自动销毁,否则使用垃圾收集。

4 参考计数和循环参考编辑

更现代的垃圾收集方案通常是基于可达性的概念——如果你没有可用的内存引用,它是可以被收集的。其他垃圾收集方案可以基于引用计数,其中一个对象负责跟踪有多少引用指向它。如果这个数字下降到零,对象应该释放自己,并允许其内存被回收。这个模型的缺点是它不能处理循环引用,这就是为什么现在大多数程序员准备接受更昂贵的标记和扫描类型系统的负担。

下面的Visual Basic代码说明了规范的引用计数内存泄漏:

Dim A, B
Set A = CreateObject("Some.Thing")
Set B = CreateObject("Some.Thing")
' At this point, the two objects each have one reference,

Set A.member = B
Set B.member = A
' Now they each have two references.

Set A = Nothing   ' You could still get out of it...

Set B = Nothing   ' And now you've got a memory leak!

End

在实践中,这个微不足道的事例会被立即发现并修复。在大多数真实的例子中,引用的循环跨越两个以上的对象,并且更难检测。

这种泄漏的一个众所周知的例子随着AJAX编程技术在网页浏览器中的兴起而凸显出来。JavaScript代码将一个DOM元素与一个事件处理程序相关联,并且在退出之前未能移除引用,这会泄漏内存(AJAX网页使给定的DOM比传统网页保持更长的时间,因此这种泄漏更加明显)。

5 效果编辑

如果一个程序有内存泄漏,并且它的内存使用在稳步增加,通常不会有立即的症状显现出来。每个物理系统都有有限的内存,如果内存泄漏没有得到控制(例如,通过重新启动泄漏程序),最终会导致问题。

大多数现代消费者桌面操作系统既有物理存储在内存微芯片中的主存储器,也有硬盘等辅助存储器。内存分配是动态的——每个进程获得它所请求的内存。活动页面被转移到主存储器中以便快速访问;根据需要,非活动页面会被推出到二级存储器以腾出空间。当一个进程开始消耗大量内存时,它通常会占用越来越多的主内存,从而将其他程序推到辅助存储器中,这通常会显著降低系统的性能。即使泄漏的程序被终止,其他程序可能需要一些时间才能交换回主内存,这样性能才能恢复正常。

当系统上的所有内存耗尽时(无论是虚拟内存还是只有主内存,例如在嵌入式系统上),分配更多内存的任何尝试都将失败。这通常会导致试图分配内存的程序自行终止,或者产生分段错误。一些程序被设计成从这种情况中恢复(可能通过返回到预先保留的内存)。遇到内存不足的第一个程序可能是内存泄漏的程序,也可能不是。

一些多任务操作系统会有特殊的机制来处理内存不足的情况,例如随机杀死进程(这可能会影响“无辜”进程),或者它杀死内存中最大的进程(这可能是导致问题的原因)。一些操作系统有每个进程的内存限制,以防止任何一个程序占用系统上的所有内存。这种安排的缺点是,操作系统有时必须重新配置,目的是允许合法需要大量内存的程序正常运行,例如处理图形、视频或科学计算的程序。

内存利用率中的“锯齿”模式:使用内存的突然下降是内存泄漏的一个可能症状。

如果内存泄漏在内核中,操作系统本身很可能会失败。没有复杂内存管理的计算机,如嵌入式系统,也可能由于持续的内存泄漏而完全失败。

如果攻击者发现一系列可能触发漏洞的操作,网络服务器或路由器等可公开访问的系统很容易遭到拒绝服务攻击。这种序列被称为利用。

内存利用率的“锯齿”模式可能是应用程序内存泄漏的指示器,尤其是指当垂直下降与该应用程序的重新启动或重启同时发生时。但是应该小心,因为垃圾收集点也可能导致这种模式,并且会显示堆的健康使用。

6 其他内存消费者编辑

需要注意的是,不断增加的内存使用不一定是内存泄漏的证据。一些应用程序会在内存中存储越来越多的信息(例如作为缓存)。如果高速缓存可能变得过大而导致问题,这可能是编程或设计错误,但不是内存泄漏,因为信息名义上仍在使用中。在其他情况下,程序可能需要不合理的大量内存,因为程序员认为内存对于特定的任务总是足够的;例如,图形文件处理器可能从读取图像文件的全部内容并将其全部存储到内存中开始,这在非常大的图像超过可用内存的情况下是不可行的。

换句话说,内存泄漏是由一种特定的编程错误引起的,如果不能访问程序代码,看到症状的人只能猜测可能有内存泄漏。如果没有这样的内部知识,最好使用“不断增加的内存使用”这样的术语。

7 一个简单的例子编辑

下面的C函数通过丢失指向分配内存的指针故意泄漏内存。可以说,一旦指针“a”超出范围,即当函数_ which _ allocates()返回而没有释放“a”时,就会发生泄漏。

#include <stdlib.h>

void function_which_allocates(void) {
    /* allocate an array of 45 floats */
    float *a = malloc(sizeof(float) * 45);

    /* additional code making use of 'a' */

    /* return to main, having forgotten to free the memory we malloc'd */
}

int main(void) {
    function_which_allocates();

    /* the pointer 'a' no longer exists, and therefore cannot be freed,
     but the memory is still allocated. a leak has occurred. */
}

参考文献

  • [1]

    ^Crockford, Douglas. "JScript Memory Leaks". Retrieved 6 November 2012..

  • [2]

    ^"Creating a memory leak with Java". Stack Overflow. Retrieved 2013-06-14..

  • [3]

    ^Mitchell, Neil. "Leaking Space". Retrieved 27 May 2017..

阅读 2646
版本记录
  • 暂无