“碎片化”确实不是一个非常准确的术语。但是我们可以肯定地说,当正在运行的应用程序需要一个n
字节块并且有n
或更多字节未使用时,我们又无法获得所需的块,那么“内存太碎片化”。
但是它 [分页] 究竟如何帮助外部分配 [我假设您的意思是碎片] ?
这里真的没有什么复杂的。外部碎片是分配块之间的空闲内存,“太小”无法满足任何应用程序要求。这是一个普遍的概念。“太小”的定义取决于应用程序。尽管如此,如果分配的块可以落在任何边界上,那么在多次分配和释放之后,很容易出现大量这样的碎片。分页以两种方式帮助解决外部碎片。
首先,它将内存细分为固定大小的相邻块 - 页面 - “足够大”,因此它们永远不会无用。同样,“足够大”的定义并不准确。但是大多数应用程序都会有很多要求可以通过单个 4k 页面来满足。由于分配页面或更少页面不会出现外部碎片问题,因此问题已得到缓解。
其次,分页硬件在应用程序页面和物理内存页面之间提供了一定程度的间接性。因此,任何可用的物理内存页面都可以用来帮助满足任何应用程序请求,无论它有多大。例如,假设您有 100 个物理页面,每隔一个物理页面(其中 50 个)分配一次。在没有页面映射硬件的情况下,可以满足的对连续内存的最大请求是 1 页。使用映射,它是 50 页。(我忽略了最初分配的没有映射物理页面的虚拟页面。这是另一个讨论。)
但是在较小的分配情况下呢?
同样,这很简单。如果分配单位是页,则任何小于页的分配都会产生未使用的部分。这是内部碎片:分配块中的不可用内存。分配单元越大(它们不必是单个页面),由于内部碎片而无法使用的内存就越多。平均而言,这将趋向于分配单元的一半。因此,尽管操作系统倾向于以页面为单位进行分配,但大多数应用程序端内存分配器向操作系统请求非常少量(通常是一个)大块(页面)。它们在内部使用更小的分配单元:4-16 字节很常见。
所以问题是如何处理外部分配[我假设你的意思是碎片]?那么(假设最坏的情况)迟早,无论有没有分页,分配和释放堆内存(不同大小)的长时间工作的应用程序会因为外部碎片而陷入低内存状态?
如果我对您的理解正确,那么您是在问碎片化是否不可避免。除非在非常特殊的条件下(例如应用程序只需要一种大小的块),否则答案是肯定的。但这并不意味着它一定是一个问题。
内存分配器使用非常有效地限制碎片的智能算法。例如,他们可以维护具有不同块大小的“池”,使用块大小最接近给定请求的池。这往往会限制内部和外部碎片。一个有据可查的真实世界示例是dlmalloc。源码也很清楚。
当然,任何通用分配器都可能在特定条件下失败。出于这个原因,现代语言(我知道 C++ 和 Ada 是两种)让您为给定类型的对象提供特殊用途的分配器。通常 - 对于固定大小的对象 - 这些可能只是维护一个预先全部覆盖的空闲列表,因此该特定情况的碎片为零,并且分配/解除分配非常快。
还有一点需要注意:通过复制/压缩垃圾收集可以完全消除碎片。当然,这需要底层语言支持,并且需要支付性能费用。复制垃圾收集器通过移动对象来压缩堆,以在运行以回收存储时完全消除未使用的空间。为此,它必须将正在运行的程序中的每个指针更新到相应对象的新位置。虽然这听起来很复杂,但我已经实现了一个复制垃圾收集器,而且还不错。算法非常酷。不幸的是,许多语言(例如 C 和 C++)的语义不允许在运行的程序中找到每个指针,这是必需的。
最激进的解决方案是禁止使用堆,但是对于具有分页、虚拟地址空间、虚拟内存等的平台真的有必要吗……唯一的问题是应用程序需要不间断地运行多年?
尽管通用分配器很好,但不能保证。对于安全关键或硬实时受限的系统来说,完全避免堆使用并不罕见。另一方面,当不需要绝对保证时,通用分配器通常就可以了。有许多系统可以使用通用分配器长时间在高负载下完美运行:碎片达到可接受的稳定状态并且不会导致问题。
还有一个问题..内部碎片化是一个模棱两可的术语吗?
该术语没有歧义,但在不同的上下文中使用。不变的是它指的是分配块内未使用的内存。
操作系统文献倾向于假设分配单元是页。例如,Linux sbrk允许您请求在任何地方设置数据段的结尾,但 Linux 分配页面而不是字节,因此从操作系统的角度来看,最后一页的未使用部分是内部碎片。
面向应用程序的讨论倾向于假设分配是在任意大小的“块”或“块”中。dlmalloc 使用大约 128 个离散的块大小,每个块都保存在自己的空闲列表中。另外,它将使用 OS 内存映射系统调用自定义分配非常大的块,因此请求和实际分配之间最多存在页面大小(减去 1 个字节)的不匹配。显然它会很多尽量减少内部碎片的麻烦。给定分配造成的碎片是请求和实际分配的块之间的差异。由于块大小如此之多,因此这种差异受到严格限制。另一方面,许多块大小增加了外部碎片问题的可能性:空闲内存可能完全由 dlmalloc 管理良好的块组成,但太小而无法满足应用程序的要求。