垃圾回收器文档和配置¶
Incminimark¶
PyPy 的默认垃圾回收器称为 incminimark - 它是一个增量式、分代移动回收器。在这里,我们希望解释一下它的工作原理以及如何调整它以适应工作负载。
Incminimark 首先在所谓的苗圃中分配对象 - 用于存放年轻对象的区域,在那里分配非常便宜,只需指针递增即可。苗圃的大小是一个非常重要的变量 - 具体取决于你的工作负载(一个或多个进程)和缓存大小,你可能需要通过PYPY_GC_NURSERY环境变量来进行实验。当苗圃已满时,会执行一次次要收集。释放的对象不再可引用,并且会直接死亡,因为它们不再被引用;另一方面,发现仍然存活的对象必须存活下来,并且会被从苗圃复制到旧一代。要么复制到竞技场,竞技场是相同大小对象的集合,要么直接使用 malloc 分配,如果它们更大。(第三类,非常大的对象,最初在苗圃之外分配,并且永远不会移动。)
由于 Incminimark 是一个增量式 GC,因此主要收集是增量式的:目标是避免出现超过 1 毫秒的暂停,但在实践中,这取决于堆的大小和特性:偶尔,可能会出现 10-100 毫秒之间的暂停。
半手动 GC 管理¶
如果程序的某些部分需要低延迟,你可能希望精确控制 GC 运行的时间,以避免意外暂停。请注意,这只会影响主要收集,而次要收集将照常工作。
如上所述,完整的 major 收集包含 N
个步骤,其中 N
取决于堆的大小;一般来说,无法预测完成收集需要多少个步骤。
gc.enable()
和 gc.disable()
控制 GC 是否自动运行收集步骤。当 GC 被禁用时,内存使用量将无限增长,除非你手动调用 gc.collect()
和 gc.collect_step()
。
gc.collect()
运行完整的 major 收集。
gc.collect_step()
运行单个收集步骤。它返回一个 GcCollectStepStats 类型的对象,与传递给相应 GC 钩子 的对象相同。以下代码大致等效于 gc.collect()
while True:
if gc.collect_step().major_is_done:
break
有关此 API 使用的实际示例,你可以查看第三方模块 pypytools.gc.custom,它还提供了一个 with customgc.nogc()
上下文管理器来标记禁止 GC 的部分。
碎片化¶
在讨论“碎片化”问题之前,我们需要先明确一下。这里涉及两种相关但不同的问题。
- 如果程序分配了大量内存,然后通过删除所有引用来释放所有内存,那么我们可能会预期看到 RSS 下降。(RSS = Linux 上的驻留集大小,如“top”所示;它是从操作系统角度来看的实际内存使用量的近似值。)这可能不会发生:RSS 可能会保持在最高值。这个问题更准确地说,是由进程没有将“空闲”内存返回给操作系统引起的。我们将这种情况称为“未返回内存”。
- 在完成上述操作后,如果 RSS 没有下降,那么至少未来的分配不应该导致 RSS 进一步增长。也就是说,只要进程还有剩余的未返回内存,它应该重复使用这些内存。如果这种情况没有发生,RSS 会变得更大,我们就会遇到真正的碎片化问题。
gc.get_stats¶
gc
模块中有一个名为 get_stats(memory_pressure=False)
的特殊函数。
memory_pressure
控制是否报告来自 GC 之外分配的对象的内存压力,这需要遍历整个堆,因此默认情况下它被禁用,因为它会带来成本。在调试神秘的内存消失时启用它。
示例调用如下所示
>>> gc.get_stats(True)
Total memory consumed:
GC used: 4.2MB (peak: 4.2MB)
in arenas: 763.7kB
rawmalloced: 383.1kB
nursery: 3.1MB
raw assembler used: 0.0kB
memory pressure: 0.0kB
-----------------------------
Total: 4.2MB
Total memory allocated:
GC allocated: 4.5MB (peak: 4.5MB)
in arenas: 763.7kB
rawmalloced: 383.1kB
nursery: 3.1MB
raw assembler allocated: 0.0kB
memory pressure: 0.0kB
-----------------------------
Total: 4.5MB
在这种特定情况下,即在启动时,GC 消耗的内存相对较少,未使用的分配内存更少。如果存在大量未返回内存或实际碎片化,“已分配”可能远高于“已使用”。一般来说,“峰值”将更接近 RSS 报告的实际内存消耗。事实上,将内存返回给操作系统是一个难题,尚未解决。在 PyPy 中,只有当一个 arena 完全空闲时才会发生这种情况——一个连续的 64 页块,每页 4 或 8 KB。对于“rawmalloced”类别来说,这种情况也很少见,至少对于 malloc()
的常见系统实现来说是如此。
各个字段的详细信息
- GC in arenas - 保存在 arenas 中的小型旧对象。如果“已分配”的数量远高于“已使用”的数量,那么我们就有未返回的内存。这里可能存在内部碎片,但可能性很小。但是,这种未返回的内存无法重复用于任何
malloc()
,包括来自“rawmalloced”部分的内存。 - GC rawmalloced - 使用 malloc 分配的大型对象。这提供了使用
malloc()
分配的当前(第一段文本)和峰值(第二段文本)内存。无法轻松报告由malloc()
引起的未返回内存或碎片化。通常,如果 RSS 远大于“GC 已分配”报告的总内存,你可以猜测存在一些碎片化,但请记住,这个总数不包括 PyPy 的 GC 完全不知道的 malloc 的内存。如果你猜测存在一些碎片化,请考虑使用 jemalloc 而不是系统 malloc。
- nursery - 为 nursery 分配的内存量,在启动时固定,通过环境变量控制
- raw assembler allocated - JIT 认为自己负责的汇编器内存量
- memory pressure, if asked for - 我们认为通过外部 malloc 分配的内存量(例如,在 SSL 上下文中加载证书存储),这些内存由 GC 对象保持活动状态,但未在 GC 中计算
GC Hooks¶
GC hooks 是用户定义的函数,每当发生特定的 GC 事件时就会调用这些函数,可用于监控 GC 活动和暂停。可以通过设置以下属性来安装 hooks
gc.hooks.on_gc_minor
- 每当发生次要收集时调用。它对应于
PYPYLOG
内部的gc-minor
部分。 gc.hooks.on_gc_collect_step
- 每当发生主要收集的增量步骤时调用。它对应于
PYPYLOG
内部的gc-collect-step
部分。 gc.hooks.on_gc_collect
- 在完成最后一次增量步骤后,当主要收集完全完成时调用。它对应于
PYPYLOG
内部的gc-collect-done
部分。
要卸载钩子,只需将相应的属性设置为 None
。要一次安装所有钩子,可以调用 gc.hooks.set(obj)
,它将在 obj
上查找方法 on_gc_*
。要一次卸载所有钩子,可以调用 gc.hooks.reset()
。
钩子调用的函数接收一个 stats
参数,其中包含有关事件的各种统计信息。
请注意,PyPy 无法在 GC 事件后立即调用钩子,而必须等到解释器处于已知状态并且调用用户定义的代码无害时才能调用。可能会发生在调用钩子之前发生多个事件:在这种情况下,您可以检查值 stats.count
以了解自上次调用钩子以来事件发生了多少次。类似地,stats.duration
包含自上次调用钩子以来 GC 为此特定事件花费的**总**时间。
另一方面,stats
对象的所有其他字段仅与系列中的**最后一个**事件相关。
on_gc_minor
钩子中 GcMinorStats
的属性为
count
- 自上次钩子调用以来发生的次要收集次数。
duration
- 自上次钩子调用以来,在次要收集中花费的总时间(以秒为单位)。
duration_min
- 自上次钩子调用以来,最快的次要收集的持续时间。
duration_max
- 自上次钩子调用以来,最慢的次要收集的持续时间。
total_memory_used
- 次要收集结束时使用的内存量(以字节为单位)。这包括在竞技场(用于 GC 管理的内存)和原始 malloc 内存(例如,numpy 数组的内容)中使用的内存。
pinned_objects
- 固定对象的数量。
on_gc_collect_step
钩子中 GcCollectStepStats
的属性为
count
,duration
,duration_min
,duration_max
- 见上文。
oldstate
,newstate
- 表示步骤前后 GC 状态的整数。
major_is_done
- 表示这是否是主要收集的最后一步的布尔值
oldstate
和 newstate
的值是以下常量之一,在 gc.GcCollectStepStats
中定义:STATE_SCANNING
, STATE_MARKING
, STATE_SWEEPING
, STATE_FINALIZING
, STATE_USERDEL
。可以通过索引 GC_STATES
元组来获取它的字符串表示形式。
on_gc_collect
钩子中 GcCollectStats
的属性为
count
- 见上文。
num_major_collects
- 自启动以来完成的主要收集总数。与
count
相反,这是一个始终增长的计数器,它不会在调用之间重置。 arenas_count_before
,arenas_count_after
- 主要收集之前和之后使用的竞技场数量。
arenas_bytes
- 垃圾回收器管理的对象使用的字节总数。
rawmalloc_bytes_before
,rawmalloc_bytes_after
- 主要回收前后,由 raw-malloced 对象使用的字节总数。
pinned_objects
- 固定对象的数量。
请注意,GcCollectStats
**没有** duration
字段。这是因为所有 GC 工作都在 gc-collect-step
中完成:gc-collect-done
仅用于提供额外的统计信息,但不会执行任何实际工作。
以下是一个使用 GC 钩子的示例
import sys
import gc
class MyHooks(object):
done = False
def on_gc_minor(self, stats):
print 'gc-minor: count = %02d, duration = %d' % (stats.count,
stats.duration)
def on_gc_collect_step(self, stats):
old = gc.GcCollectStepStats.GC_STATES[stats.oldstate]
new = gc.GcCollectStepStats.GC_STATES[stats.newstate]
print 'gc-collect-step: %s --> %s' % (old, new)
print ' count = %02d, duration = %d' % (stats.count,
stats.duration)
def on_gc_collect(self, stats):
print 'gc-collect-done: count = %02d' % stats.count
self.done = True
hooks = MyHooks()
gc.hooks.set(hooks)
# simulate some GC activity
lst = []
while not hooks.done:
lst = [lst, 1, 2, 3]
环境变量¶
PyPy 的默认 incminimark
垃圾回收器可以通过几个环境变量进行配置
PYPY_GC_NURSERY
- 育苗器大小。默认为最后一级缓存的 1/2,如果未知则为
4M
,如果最后一级缓存太小则为4M
。小值(如 1 或 1KB)对于调试很有用。 PYPY_GC_NURSERY_DEBUG
- 如果设置为非零值,将用垃圾填充育苗器,以帮助调试。
PYPY_GC_INCREMENT_STEP
- 标记步骤期间标记的内存大小。默认为育苗器大小的两倍。如果标记过高,则 GC 根本就不是增量的。最小值设置为幸存者小回收大小的 1.5 倍,因此我们始终回收任何东西。
PYPY_GC_MAJOR_COLLECT
- 主要回收内存因子。默认为
1.82
,这意味着当消耗的内存等于上次主要回收结束时实际使用的内存的 1.82 倍时触发主要回收。 PYPY_GC_GROWTH
- 主要回收阈值的最高增长率。默认为
1.4
。有助于在内存突然增长时比平时更频繁地进行回收,例如当内存使用量出现暂时峰值时。 PYPY_GC_MAX
- 最大堆大小。如果接近此限制,它将首先更频繁地进行回收,然后引发 RPython MemoryError,如果这还不够,则会以致命错误使程序崩溃。尝试使用
1.6GB
之类的值。 PYPY_GC_MAX_DELTA
- 主要回收阈值永远不会设置为回收后实际使用的数量的
PYPY_GC_MAX_DELTA
以上。默认为总 RAM 大小的 1/8(在 32 位系统上限制为最多 2/3/4GB)。尝试使用200MB
之类的值。 PYPY_GC_MIN
- 当内存大小低于此限制时,不要进行回收。有助于避免在非常小的程序中花费所有时间进行 GC。默认为育苗器的 8 倍。
PYPY_GC_DEBUG
- 启用围绕回收的额外检查,这些检查对于正常使用来说太慢了。值为
0
(关闭)、1
(在主要回收时)或2
(也在小回收时)。 PYPY_GC_MAX_PINNED
- 任何时间点上固定对象的最大数量。默认为一个保守的值,具体取决于育苗器大小和育苗器内最大对象大小。通过将其设置为 0 来进行调试很有用。