2.40. 内存与垃圾回收

MicroPython 管理内存的方式与 CPython 相同:每个对象都驻留在 堆(heap) 上,而 垃圾回收器(GC)会释放不再被任何东西引用的对象。在一台只有几百千字节 RAM 的设备上,留意堆是如何被使用的偶尔很有必要;而在桌面 Python 上,则很少需要。

2.40.1.

堆是运行时分发给 Python 对象的那块 RAM 区域 —— 在大多数 OpenMV 摄像头上它实际上分散在不止一个物理内存池中。每次 Python 表达式创建一个新对象(列表、字符串、字典、元组,任何不是小整数或单例的东西)时,都会从堆中取出一块字节来容纳它。当 GC 注意到某个对象不可达时,就把这块内存归还到它原来所属的空闲池。

gc 模块中有两个值得了解的函数:

  • gc.mem_free() —— 此刻堆上空闲字节的近似数量。

  • gc.collect() —— 立即运行一个回收周期,而不是等待运行时去触发一个。

import gc

print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())

确切的数字取决于具体构建;重要的是变化的 方向:分配大的东西会减少 mem_free,而丢弃引用再加一次 gc.collect 则会把堆归还回来。

2.40.2. 碎片化

空闲池不会神奇地保持连续。随着对象在不同的生命周期里来来去去,即便空闲空间的总量仍然很大,它也会碎裂成越来越小的小块。分配一个比最大单个空闲块还要大的对象会以 MemoryError 失败 —— 即使从技术上讲总的空闲 RAM 是足够的。

堆的两种视图。之前:一大块连续的 空闲区域。之后:许多小的空闲区域与 已分配的块交错在一起,单个都不足以 容纳一次大的分配。

同样总量的空闲 RAM 能否容纳一个大缓冲区(左)或拒绝容纳它(右),取决于它碎片化的程度。

碎片化主要是那些长时间运行、不断分配和释放不同大小缓冲区的脚本所要担心的。最有效的单一缓解办法是在程序开始附近、在大量短命分配有机会把它们打散之前,预先分配 长期存活的缓冲区。

2.40.3. 预分配

两种模式能让堆表现良好:

  • 一次性分配固定大小的缓冲区并复用它们,而不是每次迭代都构建一个新列表或 bytearray

  • 把常量和查找表从内层循环里提出来,这样它们只被创建一次。

buf = bytearray(64)        # one allocation, reused below

def fill(value):
    for i in range(len(buf)):
        buf[i] = value

fill(0)
fill(255)

对比一个在循环内部创建新 bytearray 的版本:每次迭代都产生 GC 稍后必须清理的垃圾。预分配的版本则完全不产生垃圾。

2.40.4. 何时调用 gc.collect

gc.collect() 通常是自动的 —— 当分配找不到足够空闲内存时运行时会触发它。手动调用它在两种情形下很有用:

  • 在一大批对象刚刚离开作用域之后立即调用,以便马上释放它们,而不是等下一次分配来承担这个成本。

  • 在一段需要已知最大空闲内存量的代码之前立即调用,以避免 GC 在某个对时间敏感的操作进行到一半时触发。

到处撒上 gc.collect 调用并不会让程序更快 —— 回收本身也要花时间。请有意识地使用它,用在那些一次计划外回收的成本会比一次你有意运行的回收更糟糕的地方。