三种类型的内存泄漏

大家好

我们长期以来一直在寻找有关代码优化的永恒畅销书籍,但仅获得了第一批结果,但我们很高兴为您带来本·沃森的传奇著作“ 编写高性能.NET代码 ”的翻译工作刚刚完成。 在商店中-暂定于四月,留意广告。

今天,我们为您提供由Nelson Ilheidzhe (Strike)撰写的有关最紧迫的内存泄漏类型的纯实用文章。

因此,您需要花费较长时间才能完成的程序。 也许,您将不难理解,这肯定是内存泄漏的迹象。
但是,“内存泄漏”到底是什么意思? 以我的经验,显式的内存泄漏分为三个主要类别,每个类别都有一个特殊的行为特征,而调试每个类别都需要特殊的工具和技术。 在本文中,我想描述所有三个类并建议如何正确识别
您正在处理哪个类,以及如何查找泄漏。

类型(1):分配了无法访问的内存片段

这是C / C ++中的经典内存泄漏。 有人使用newmalloc分配了内存,在使用完内存后没有调用freedelete释放内存。

 void leak_memory() { char *leaked = malloc(4096); use_a_buffer(leaked); /* ,   free() */ } 

如何确定泄漏是否属于此类别

  • 如果您是用C或C ++编写的,尤其是在C ++中编写的,而没有广泛使用智能指针来控制内存段的生存期,那么这就是我们首先考虑的选择。
  • 如果程序在具有垃圾回收的环境中运行,则本机代码扩展可能会引发这种类型的泄漏,但是,必须首先消除类型(2)和(3)的泄漏。

如何发现这样的泄漏

  • 使用ASAN 。 使用ASAN。 使用ASAN。
  • 使用其他检测器。 我尝试使用Valgrind或tcmalloc工具来处理一堆东西,其他环境中也有其他工具。
  • 一些内存分配器允许转储堆配置文件,该配置文件将显示所有未分配的内存区域。 如果您有泄漏,则一段时间后,几乎所有有效放电都会从中泄漏出来,因此找到它可能并不困难。
  • 如果所有其他方法均失败,请转储内存转储并尽可能进行仔细检查 。 但是绝对不应该从这个开始。

类型(2):计划外的长期内存分配

在这种情况下,这种情况并不是传统意义上的“泄漏”,因为仍然保留了从某处到该内存的链接,因此最终可以将其释放(如果程序设法在不占用所有内存的情况下到达那里)。
出于许多特定原因,可能会出现此类情况。 最常见的是:

  • 国家在全球结构中的无意积累; 例如,HTTP服务器将每个接收到的Request对象写入全局列表。
  • 没有经过深思熟虑的过时策略的缓存。 例如,一个ORM高速缓存会缓存在迁移过程中处于活动状态的每个单个装入的对象,其中将无例外地装入表中存在的所有记录。
  • 电路中捕获了过多的状态。 这种情况在Java Script中尤为常见 ,但在其他环境中也可能发生。
  • 从广义上讲,数组或流中每个元素的无意保留都会被假定为在线流处理这些元素。

如何确定泄漏是否属于此类别

  • 如果程序在具有垃圾回收的环境中运行,那么这是我们首先考虑的选项。
  • 将垃圾收集器统计信息中显示的堆大小与操作系统生成的可用内存大小进行比较。 如果泄漏属于这一类,那么这些数字将是可比的,并且最重要的是,随着时间的流逝,这些数字将相互跟踪。

如何发现这样的泄漏

使用环境中可用的探查器或堆转储工具。 我知道Python中有guppy或Ruby中有memory_profiler ,我也直接在Ruby中编写了ObjectSpace

类型(3):空闲但未使用或无法使用的内存

这个类别很难描述,但恰恰是理解和考虑这个类别是最重要的。

此类泄漏发生在内存中的灰色区域(从VM或运行时环境内部的分配器的角度来看是“空闲”)与内存(从操作系统的角度来看是“空闲”)之间的灰色区域中。 发生此现象的最常见(但不是唯一)原因是堆碎片 。 某些分配器只是在分配内存后才将其带回操作系统,而不将其返回给操作系统。

可以用一个用Python编写的简短程序的示例来考虑这种情况:

 import sys from guppy import hpy hp = hpy() def rss(): return 4096 * int(open('/proc/self/stat').read().split(' ')[23]) def gcsize(): return hp.heap().size rss0, gc0 = (rss(), gcsize()) buf = [bytearray(1024) for i in range(200*1024)] print("start rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) buf = buf[::2] print("end rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) 

我们分配200,000个1-kb缓冲区,然后保存每个后续缓冲区。 从操作系统的角度以及我们自己的Python垃圾收集器的角度来看,每秒都会显示内存状态。

在我的笔记本电脑上,我得到如下信息:

start rss=232222720 gcsize=11667592
end rss=232222720 gcsize=5769520


我们可以确保Python实际上释放了一半的缓冲区,因为gcsize电平从峰值下降了近一半,但无法将该内存的一个字节返回给操作系统。 释放的内存仍可供同一Python进程访问,但不能供该计算机上的任何其他进程访问。

这样的空闲但未使用的内存碎片可能既有问题又无害。 如果Python程序以此方式运行,然后分配少量的1kb片段,则可以简单地重用此空间,一切都很好。

但是,如果我们在初始设置期间进行了此操作,并且随后将内存分配到了最低限度,或者如果所有后续分配的片段每个都是1.5kb,并且不适合预先留在这些缓冲区中,那么以这种方式分配的所有内存将始终处于空闲状态将被浪费。

在特定环境中,即在使用诸如Ruby或Python之类的语言的多进程服务器系统中,此类问题尤为重要。

假设我们建立了一个系统,其中:

  • 在每台服务器上,使用N个单线程工作线程胜任服务请求。 取N = 10为精度。
  • 通常,每个员工都有几乎恒定的内存量。 为了提高准确性,我们占用500MB。
  • 在某些低频率下,我们收到的请求比中位请求需要更多的内存。 为了准确起见,我们假设每隔一分钟我们收到一个请求,该请求的执行时间另外需要额外的1GB内存,并且在处理请求时,将释放该内存。

每分钟一次,这样的“ cetacean”请求到达,我们将其处理工作委托给10个工人之一,例如,随机地: ~random 。 理想情况下,在处理此请求期间,该员工应分配1GB RAM,并在工作结束后将此内存返回给操作系统,以便以后可以重用。 要按照此原则无限处理请求,服务器仅需要10 * 500MB + 1GB = 6GB RAM。

但是,我们假设由于碎片或其他原因,虚拟机将永远无法将该内存返回给操作系统。 也就是说,操作系统需要的RAM数量等于一次必须分配的最大内存量。 在这种情况下,当特定员工为此类资源密集型请求提供服务时,此类进程在内存中占据的区域将永远膨胀整整GB。

启动服务器时,您将看到使用的内存量为10 * 500MB = 5GB。 第一个大请求到达后,第一个工作程序将占用1GB内存,然后不将其退还给您。 已使用的内存总量将跳到6GB。 以下传入请求有时可能会重定向到先前处理“鲸鱼”的进程,在这种情况下,使用的内存量不会改变。 但是有时候,如此大的请求将被传递给另一名员工,因此,该内存将再增加1GB,依此类推,直到每个员工都有机会至少处理一次这样的大请求。 在这种情况下,通过这些操作,您将占用多达10 *(500MB + 1GB)= 15GB的RAM,这比理想的6GB还要多! 此外,如果考虑服务器群随时间的使用情况,您会发现使用的内存量是如何从5GB逐渐增加到15GB的,这非常让人联想到“实际”泄漏。

如何确定泄漏是否属于此类别

  • 将垃圾收集器统计信息中显示的堆大小与操作系统生成的可用内存大小进行比较。 如果泄漏属于此(第三类)类别,则数字将随着时间的推移而发生变化。
  • 我喜欢配置我的应用程序服务器,以便这两个数字在我的时间序列基础结构中定期出现,因此在它们上显示图形非常方便。
  • 在Linux上,在/proc/self/stat字段24中查看操作系统状态,并通过特定于语言或虚拟机的API查看内存分配器。

如何发现这样的泄漏

如前所述,该类别比以前的类别更具隐蔽性,因为即使所有组件都按“预期”方式工作,也会经常出现此问题。 但是,有许多有用的技巧可以帮助减轻或减少此类“虚拟泄漏”的影响:

  • 更频繁地重新启动流程。 如果问题发展缓慢,那么也许每15分钟或每小时重新启动所有应用程序进程可能并不困难。
  • 甚至更根本的方法:您可以教导所有进程一旦它们在内存中占据的空间超过某个阈值或增长一个预定值,就可以独立地重新启动。 但是,请尝试预见整个服务器群无法启动自发同步重启。
  • 更改内存分配器。 从长远来看, tcmallocjemalloc通常比默认分配器更好地处理碎片,并且使用LD_PRELOAD变量进行试验非常方便。
  • 找出您是否有个别查询消耗的内存比其余查询多得多。 在Stripe,我们的API服务器在处理每个API请求之前和之后测量RSS(恒定内存消耗)并记录增量。 然后,我们可以轻松地查询日志聚合系统,以确定是否存在可用于注销内存消耗突发的此类终端和用户(和模式)。
  • 调整垃圾回收器/内存分配器。 其中许多具有可自定义的参数,可让您指定这种机制将内存返还至操作系统的积极程度,消除碎片的优化程度。 还有其他有用的选项。 这里的一切也非常复杂:请确保您完全了解要测量和优化的内容,并尝试在适当的虚拟机上找到专家并向其咨询。

Source: https://habr.com/ru/post/zh-CN432072/


All Articles