内存管理¶
与 C/C++ 等编程语言不同,MicroPython 通过支持自动内存管理向开发者隐藏了内存管理的细节。自动内存管理是操作系统或应用程序用来自动管理内存分配与释放的一种技术。它消除了诸如忘记释放分配给对象的内存等难题。自动内存管理还避免了使用已释放内存这一严重问题。自动内存管理有多种形式,其中之一就是垃圾回收(GC)。
垃圾回收器通常承担两项职责:
在可用内存中分配新对象。
释放未使用的内存。
GC 算法有很多种,但 MicroPython 采用 标记-清除 策略来管理内存。该算法包含一个标记阶段,遍历堆并标记所有存活对象;而清除阶段则遍历堆,回收所有未被标记的对象。
MicroPython 中的垃圾回收功能可通过 gc 内置模块使用:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
即使调用了 gc.disable(),仍可使用 gc.collect() 触发回收。
从 C 代码使用 MicroPython 内存¶
在编写从“Python 堆”分配内存的 C 代码时(即 m_malloc()、m_malloc0()、m_free() 等函数),需要了解垃圾回收器的相关知识。
垃圾回收器的标记阶段会从以下根(root)开始扫描指向堆内存的存活指针:
主 Python 运行时(或 REPL)的栈。
对于在原生操作系统线程或任务之上实现 Python 线程的移植版本,还包括每个“Python 线程”的栈。
在 C 代码中使用宏
MP_REGISTER_ROOT_POINTER定义的“根指针”。这是让静态作用域的指针指向 Python 堆的推荐方式。使用
m_tracked_calloc()、m_tracked_realloc和m_tracked_free()函数进行的受跟踪分配。这些特殊函数允许分配一块始终被垃圾回收器视为存活的内存。与 C 中的内存分配类似,这块内存只有通过调用m_tracked_free()或软复位才会被释放。每次受跟踪分配都会带来少量的内存占用和运行时开销。该特性并非在所有移植版本上都默认启用。
随后,垃圾回收器会从根指针出发,递归地扫描并标记它们所指向的所有内存,直到所有地址都被遍历完毕。这足以找到 MicroPython 运行时仍在使用的所有 Python 对象。
然而,以下内存不会被垃圾回收器扫描,因而可能被过早释放:
包含指向堆内存指针的静态或全局 C 变量。
不指向已分配缓冲区“头部”(即
m_malloc()返回的确切地址)、而是指向已分配缓冲区内部某个地址的指针(例如指向嵌套结构体的指针)。出于性能考虑,在这种情况下垃圾回收器不会标记其外层缓冲区。任何未运行 Python 代码、也未被手动注册为“Python 线程”的线程或 RTOS 任务的栈(针对支持原生线程或任务的移植版本)。
在这些场景中避免释放后使用(use-after-free)的方法:
使用受跟踪分配 API
m_tracked_calloc()、m_tracked_realloc()和m_tracked_free()。注册一个根指针(见上文),而不是将指针存储在静态变量中。
重构代码,例如设计一个 API,让 Python 代码初始化一个单例 Python 对象(用 C 实现),由它持有所有相关指针,而不是将它们存放在静态变量中。
对象模型¶
所有 MicroPython 对象都通过 mp_obj_t 数据类型来引用。它通常是字大小的(即与目标架构上的指针大小相同),一般为 32 位(STM32、RP2、nRF、Unix x86)或 64 位(Unix x64)。对于某些对象表示形式,它也可能大于一个字的大小,例如 OBJ_REPR_D 在 32 位架构上拥有 64 位大小的 mp_obj_t。
一个 mp_obj_t 表示一个 MicroPython 对象,例如整数、浮点数、类型、字典或类实例。某些对象(如布尔值和小整数)的值直接存储在 mp_obj_t 值中,无需额外内存。其他对象的值则存储在内存的别处(例如垃圾回收堆上),其 mp_obj_t 中包含指向该内存的指针。mp_obj_t 的一部分是标签(tag),用于指明它是哪种类型的对象。
有关可用表示形式的具体细节,请参阅 py/mpconfig.h。
指针标签
由于指针是字对齐的,当它们被存储在 mp_obj_t 中时,这个对象句柄的低位将为零。例如在 32 位架构上,最低 2 位将为零:
********|********|********|******00
这些位被保留用于存储标签。标签用于存储额外信息,而不是在对象中引入一个新字段来存储该信息——后者可能效率低下。在 MicroPython 中,标签会告诉我们处理的是小整数、内化(interned,小型)字符串还是具体对象,而这三者各自适用不同的语义。
对于小整数,其映射如下:
********|********|********|*******1
其中星号保存实际的整数值。对于内化字符串或立即对象(例如 True),mp_obj_t 值的布局分别为:
********|********|********|*****010
********|********|********|*****110
而既非以上任何一种的具体对象则采用如下形式:
********|********|********|******00
这里的星号对应该具体对象在内存中的地址。
对象的分配¶
小整数的值直接存储在 mp_obj_t 中,并就地分配,而不是分配在堆或其他地方。因此,创建小整数不会影响堆。对于其文本数据已存储在别处的内化字符串,以及 None、False 和 True 等立即值,情况也是如此。
其他所有作为具体对象的内容都分配在堆上,其对象结构会在对象头中保留一个字段,用于存储对象的类型。
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
堆的最小分配单位是块(block),其大小为四个机器字(在 32 位机器上为 16 字节,在 64 位机器上为 32 字节)。还有另一个同样分配在堆上的结构,用于跟踪每个块中对象的分配情况。该结构称为位图(bitmap)。

位图跟踪一个块是“空闲”还是“正在使用”,并使用两个比特位来记录每个块的这一状态。
标记-清除垃圾回收器管理在堆上分配的对象,同时也利用位图来标记仍在使用的对象。完整的实现细节请参阅 py/gc.c。
分配:堆布局
堆的组织方式是由多个池(pool)中的块构成。一个块可以具有不同的属性:
ATB(分配表字节,allocation table byte): 若已设置,则该块为普通块
FREE: 空闲块
HEAD: 块链的头部
TAIL: 处于块链的尾部
MARK : 已标记的头部块
FTB(终结器表字节,finaliser table byte): 若已设置,则该块带有终结器