1.前置概念
MicroPython 的核心应用场景是微控制器(如树莓派 Pico、ESP32、ESP8266),这类设备的硬件资源与电脑 / 服务器存在数量级的差距 —— 通常 RAM(运行内存)只有几十 KB 到几百 KB,Flash(存储内存)也仅几 MB。而 Python 代码本身具有动态性,运行时会占用一定的内存资源,因此在微控制器上使用 MicroPython 时,内存优化是保证程序稳定运行的关键。本文将详细介绍 MicroPython 内存优化的方法,其中会对新手不易理解的专有名词进行标注解释,同时为核心知识点提供可在 REPL(MicroPython 交互式命令行)中运行的测试代码,帮助你边学边验证。
在学习具体优化方法前,先理解以下核心专有名词,能让后续内容更易理解:
- RAM(随机存取存储器):又称运行内存,是微控制器用于临时存储运行中的代码、数据的区域,特点是读写速度快,但断电后数据丢失,且容量极小(树莓派 Pico 的 RAM 为 264KB)。
- Flash(闪存) :又称存储内存,是微控制器用于永久存储固件、用户代码、数据的区域,特点是断电后数据不丢失,容量比 RAM 大,但读写速度较慢(树莓派 Pico 的 Flash 为 2MB)。
- 字节码:MicroPython 不会直接执行人类编写的源码(.py 文件),而是先将源码编译为一种介于源码和机器码之间的中间代码(即字节码),再由 MicroPython 解释器执行。字节码的体积更小,执行效率更高。
- 固件:烧录到微控制器 Flash 中的底层软件,包含 MicroPython 解释器、硬件驱动等核心功能,是 MicroPython 运行的基础。
- REPL:即 Read-Eval-Print Loop(读取 - 执行 - 打印循环),是 MicroPython 的交互式命令行工具(如 Thonny 的 Shell 面板、串口工具的交互窗口),可以逐行输入代码并立即执行,是新手调试、测试代码的必备工具。
- SPI 总线:一种串行外设接口(Serial Peripheral Interface),是微控制器与外部设备(如 SD 卡、传感器)通信的常用协议,具有接线简单、传输速度快的特点。
2.优化方法
2.1 基本概念
在学习具体优化方法前,先理解以下核心专有名词,能让后续内容更易理解:
- RAM(随机存取存储器) :又称运行内存,是微控制器用于临时存储运行中的代码、数据的区域,特点是读写速度快,但断电后数据丢失,且容量极小(树莓派 Pico 的 RAM 为 264KB)。
- Flash(闪存) :又称存储内存,是微控制器用于永久存储固件、用户代码、数据的区域,特点是断电后数据不丢失,容量比 RAM 大,但读写速度较慢(树莓派 Pico 的 Flash 为 2MB)。
- 字节码:MicroPython 不会直接执行人类编写的源码(.py 文件),而是先将源码编译为一种介于源码和机器码之间的中间代码(即字节码),再由 MicroPython 解释器执行。字节码的体积更小,执行效率更高。
- 固件:烧录到微控制器 Flash 中的底层软件,包含 MicroPython 解释器、硬件驱动等核心功能,是 MicroPython 运行的基础。
- REPL:即 Read-Eval-Print Loop(读取 - 执行 - 打印循环),是 MicroPython 的交互式命令行工具(如 Thonny 的 Shell 面板、串口工具的交互窗口),可以逐行输入代码并立即执行,是新手调试、测试代码的必备工具。
- SPI 总线:一种串行外设接口(Serial Peripheral Interface),是微控制器与外部设备(如 SD 卡、传感器)通信的常用协议,具有接线简单、传输速度快的特点。
2.2 安装 SD 卡
支持 MicroPython 的开发板可以通过插入 SD 卡的方式扩展内存,首先需要将卡格式化为 FAT/FAT32格式(一种通用的文件系统格式,被绝大多数设备支持,也是 MicroPython 识别 SD 卡的必要格式) 。通常使用 SPI 总线 挂在 SD 卡,插卡后 MicroPython 会从 SD 卡启动,如果 SD 卡上有 boot.py 和 main.py,在启动时也会自动执行。
我们也可以从内部 flash 启动而又使用 SD 卡保存数据,这时需要在 SD 卡根目录中设置一个 SKIPSD 文件,当系统启动时检查到 SD 上存在这个文件,就会忽略 SD 卡,仍然从内部 Flash 启动。
关于 MicroPython 在 SD 卡上的使用,可以看下面的教程:
08 SPI 串行外设接口-文档教程初稿
在树莓派 pico 上使用 SD 卡可以参考教程:
- Micropython SD 卡操作 https://blog.csdn.net/gene8888/article/details/89599251
- 使用 SPI 方式挂载 SD 卡 https://pico.nxez.com/2018/12/30/mount-the-sd-card-using-spi.html
- 树莓派 Pico 与 MicroSD 卡模块接口 https://blog.csdn.net/yuanzywhu/article/details/131592815
- MicroPython 设置忽略 SD 卡启动 https://pico.nxez.com/2018/12/21/micropython-settings-ignore-sd-card-boot.html
2.3 使用 frozen 模块和 frozen 字节码
实际开发中我们往往有多个.py 文件存放不同的代码,main.py 文件运行主要的业务流程,main.py 通过 import 语句引用其他.py 文件 / 模块 / 包完成整个工作(例如其他传感器的类)。main.py 文件和其他文件往往都处于开发板文件系统的根目录。
从文件系统上的 Python 文件加载模块和包来存储和运行代码有一些很大的限制,这些 Python 代码必须由 MicroPython 的解释器加载和处理,该过程需要耗费时间和内存,甚至有的情况下这些代码文件太大,无法加载到 Flash 内存并由 MicroPython 的解释器处理。Frozen 模块(冻结模块)和 Frozen 字节码(冻结字节码)可以将 Python 代码编译为 native 代码 / 字节码并将其和固件存储,这样做可以压缩内存。一旦代码被冻结,MicroPython 就可以快速加载和解释它,而无需太多的内存和处理时间。
2.3.1 Python 字节码
MicroPython 代码先被编译为字节码后,再由解释器来执行字节码,MicroPython 的字节码是一种类似汇编指令的中间语言,一个 MicroPython 语句会对应若干字节码指令,解释器顺序执行字节码指令,从而完成程序执行。
代码被预编译为字节码,避免了在加载时编译 MicroPython 源代码的需要。字节码可以直接从 Flash 执行,而不需要复制到 RAM 中。同样,任何常量对象(如字符串、元组等)也从 ROM 加载。这可以使得更多的内存可用于应用程序。在没有文件系统的设备上,这是加载 Python 代码的唯一方式。
2.3.2 生成冻结模块和字节码的步骤
一个生成冻结模块和字节码的步骤通常如下:
- 将要冻结的模块放在指定目录中(取决于模块是作为源还是字节码冻结)
- 构建固件。可能需要特定命令来构建任一类型的冻结代码
2.3.3 MicroPython 的 frozen 的主要特点
MicroPython 的 frozen 的主要特点是:
- 它可以通过一个 “清单” 文件来列出要冻结到固件中的 Python 文件,清单文件是一个包含一系列函数调用的 Python 文件,可以在板子定义中编写,也可以编写一个独立的清单文件,并与现有的板子定义一起使用
- 它可以定义对来自 micropython-lib 的库以及文件系统上的 Python 文件的依赖关系,以及对其他清单文件的依赖关系
- 它可以让用户在 Python 中创建和操作各种类型的对象,如数字、字符串、列表、字典、函数、模块等
使用冻结模块和冻结字节码可以参考教程:
- Freezing the byte-code of a module and include it in the firmware doesn't work https://github.com/bbcmicrobit/micropython/issues/555
- Frozen Modules https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules
- How to freeze module for ESP32 https://forum.micropython.org/viewtopic.php?t=12268
2.4 使用.mpy 文件
将 python 模块预编译为字节码(也称为 .mpy 文件(MicroPython 的预编译字节码文件格式,区别于标准 Python 的.pyc 文件)),然后将其复制到开发板上。这样做的优点是可以跳过 开发板上的预编译阶段,从而避免在此过程中没有 RAM 资源。不幸的是,这种方法仍然需要开发板将模块加载到 RAM 中。
将 python 模块转换为 .c 文件,该文件被编译到固件本身中。具有上述方法的优点 + 具有从闪存运行模块而不是将其加载到 RAM 中的优点。
.mpy 文件生成与使用步骤可以参考:如何加快 MicroPython 的运行速度
2.5 使用 const 常量
2.5.1 MicroPython 命名空间和作用域
MicroPython 中所有加载到 RAM 的模块都会放在 sys.modules,sys.modules 是一个全局字典,从 python 程序启动就加载到了内存,用于保存当前已导入 (加载) 的所有模块名和模块对象。在 MicroPython 的模块查找中,它起到缓存作用,避免了模块的重复加载。
程序在导入某个模块时,会首先查找 sys.modules 中是否包含此模块名,若存在,则只需将模块的名字加入到当前模块的 Local 命名空间中;若不存在,则需要从 sys.path 目录中按照模块名称查找模块文件,找到后将模块加载到内存,并加入到 sys.modules 字典,最后将模块的名字加入当前模块的 Local 命名空间中。
接下来,我们在终端的 REPL 中进行测试:
import sys# sys.modules显示当前已导入 (加载) 的所有模块名和模块对象print("Initial modules:")print(list(sys.modules.keys()))# 定义全局变量global_var = "I am a global variable"# enclosing_var 闭包外层变量# local_var 函数inner_function内部局部变量# len 是内置函数# 尝试修改全局变量global_var (不使用global会创建局部变量)# 闭包外层无法访问内部局部变量defscope_demo(): enclosing_var = "I am an enclosing variable (outer of closure)"definner_function(): local_var = "I am a local variable"print("Local variable:", local_var)print("Enclosing variable:", enclosing_var)print("Global variable:", global_var)print("Built-in function example:", len("test"))try: global_var = "Trying to modify"except:pass inner_function()try:print(local_var)except:print("Cannot access local variable")# 使用global修改全局变量defmodify_global():global global_var global_var = "Modified global variable"print("Inside function:", global_var)# 多层嵌套中的nonlocal使用# 内部middle()函数和inner()函数中修改嵌套作用域中的变量outer_vardefnonlocal_demo(): outer_var = "Outer layer"defmiddle():nonlocal outer_var outer_var = "Modified by middle layer"definner():nonlocal outer_var outer_var = "Modified by inner layer" inner()print("In middle:", outer_var) middle()print("In nonlocal_demo:", outer_var)# 作用域演示print("\n=== Scope Demonstration ===")scope_demo()print("Access in global scope:", global_var)# 修改全局变量print("\n=== Modify Global Variable ===")modify_global()print("In global scope:", global_var)# nonlocal演示print("\n=== nonlocal Demonstration ===")nonlocal_demo()# 尝试访问内置作用域import builtinsprint("Built-in module:", builtins)
终端运行,输出如下:




2.5.2 const 常量的定义与优化原理
常量是指在程序执行期间不会发生变化的值,一般用于存储不可更改的数据,比如数学常数、配置信息等。在单片机中常量会保存在 ROM/Flash 闪存中,可以节省 RAM 空间。
MicroPython 中的关键字 const 用于声明表达式是常量,以便编译器对其进行优化,在将常量分配给变量的两种情况下,编译器将避免通过替换常量的文字值来编写对常量名称的查找。这可以节省字节码,从而节省 RAM。
不过需要注意的是,使用关键字 const 时候,需要使用 mpy-cross 工具编译为 mpy 文件才能发挥其作用。
from micropython import const# 垃圾回收模块,用于查看内存使用情况import gc# 第一步:查看初始内存使用情况# 强制垃圾回收,释放无用内存gc.collect()initial_free = gc.mem_free()print(f"Initial free RAM: {initial_free} bytes")# 第二步:定义const常量# 公开常量(模块外能访问,仅占极少量RAM)CONST_X = const(123)CONST_Y = const(2 * 123 + 1)# 私有常量(以下划线开头,模块外不能访问,完全不占用RAM)_COLS = const(0x10)ROWS = const(33)# 第三步:使用常量a = ROWSb = _COLSprint(f"CONST_X: {CONST_X}, CONST_Y: {CONST_Y}")print(f"a: {a}, b: {b}")# 第四步:查看定义常量后的内存使用情况gc.collect()after_free = gc.mem_free()print(f"Free RAM after defining constants: {after_free} bytes")print(f"RAM change: {after_free - initial_free} bytes (positive means memory freed)")# 补充:对比普通变量与const常量的区别# 普通变量,存储在RAM中normal_var = 123 gc.collect()after_normal = gc.mem_free()print(f"Free RAM after defining normal variable: {after_normal} bytes")print(f"Normal variable uses {after_free - after_normal} more bytes than const")
ROWS 值将占用至少两个机器字,全局字典中的键和值各一个。字典中的存在是必要的,因为另一个模块可能会导入或使用它。可以通过在名称前面添加下划线来保存此 RAM,如 _COLS 所示:此符号在模块外部不可见,因此不会占用 RAM。
终端运行结果如下:
const () 的参数必须是任何在编译时计算结果为整数的值。
from micropython import constROWS = const(33)# 下面用法报错COLS = const(0x10+ROWS)# 正确用法COLS = const(0x10+33)
终端输出如下:
2.6 减少不必要的对象创建
在 MicroPython 中,对象(如字符串、列表、字典、字节数组、数值容器等)的创建会占用 RAM 资源。由于微控制器的 RAM 容量极小,频繁创建和销毁对象不仅会直接消耗内存,还会导致内存碎片化(即 RAM 中出现大量小的空闲内存块,无法被大对象使用),最终引发内存不足的问题。
需要注意的是,标准 Python 中的 sys.getsizeof() 函数在 MicroPython 中大部分平台不支持(这是 MicroPython 的特性,为了精简固件),因此我们可以通过 struct 模块将变量 / 数据打包为字节流,再通过 len() 函数获取字节流的长度,以此来测试数据占用的字节大小;同时结合 gc 模块(垃圾回收)查看 RAM 的实际使用变化,完成优化前后的对比。
2.6.1 字符串操作优化(避免临时字符串对象)
MicroPython 中的字符串是不可变对象,使用 + 拼接字符串时,每一次 + 都会创建一个临时字符串对象(比如 "a"+"b"+"c" 会先创建 "ab",再创建 "abc",共 2 个临时对象),这些临时对象会占用额外的 RAM,且会被垃圾回收器频繁清理,影响性能。
优化思路:
- 使用
join() 方法(仅创建一个最终字符串对象); - 使用多行字符串(
""" 或 ''',编译时合并)。
# 静态字符串拼接(编译时合并,无临时对象)static_str = "MicroPython" "Memory" "Opt"print("Static string:", static_str)# 动态字符串(推荐format,减少临时对象)temp = 25.5press = 101325dynamic_str = "Temp: {:.2f}, Press: {:d}".format(temp, press)print("Dynamic string:", dynamic_str)
2.6.2 硬件操作中的缓冲区复用(UART/SPI/I2C 场景)
在 MicroPython 的硬件操作中(如 SPI 读取传感器数据、UART 接收数据),频繁创建新的字节缓冲区(bytearray/bytes)会占用大量 RAM。例如,每次读取传感器都创建 buf = bytearray(10),循环 100 次就会创建 100 个缓冲区对象。
我们可以提前分配一个缓冲区,在循环 / 多次操作中复用,仅创建一次对象,彻底避免临时缓冲区的创建。
下面我们预分配串口接收缓冲区,循环中复用,避免重复创建 bytearray。
from machine import UART, Pin# 初始化UARTuart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))# 预分配缓冲区(仅创建1次)uart_buf = bytearray(16)# 循环接收(复用缓冲区)for _ inrange(5): uart.readinto(uart_buf)print("UART data:", uart_buf)
下面,我们预分配 SPI 读写缓冲区,复用减少内存开销。
from machine import SPI, Pin# 初始化SPIspi = SPI(0, baudrate=1000000, sck=Pin(2), mosi=Pin(3), miso=Pin(4))# 预分配缓冲区spi_buf = bytearray(8)# 循环读写(复用缓冲区)for _ inrange(5): spi.readinto(spi_buf)print("SPI data:", spi_buf)
2.6.3 数值存储优化(用 struct/bytearray 代替列表)
在 MicroPython 中,列表存储数值(如 [1, 2, 3, 255])会占用较多 RAM,因为列表中的每个整数对象都有额外的内存开销(比如一个 int 类型在 MicroPython 中占 4 字节,而列表本身还有指针开销)。
优化思路:
- 使用
struct 模块将多个数值打包为字节流(比如将 4 个整数打包为 4 字节的字节流); - 使用
bytearray(可变字节数组)存储小数值(0-255),每个元素仅占 1 字节,远少于列表中的整数; str 与 bytes 可通过 encode()/decode() 转换,但转换会创建新对象,需避免频繁操作。
# 1. str vs bytes(内存占用一致,ASCII场景)# String (ASCII)s = 'the quick brown fox'# Bytes (1 byte per char)b = b'the quick brown fox'print("String:", s)print("Bytes:", b)# 2. 字符串与字节的转换(注意:转换会创建新对象)# str -> bytess_to_b = s.encode() # bytes -> strb_to_s = b.decode() print("Str->Bytes:", s_to_b)print("Bytes->Str:", b_to_s)# 3. bytes支持字符串方法(如lstrip)foo = b' empty whitespace'foo_stripped = foo.lstrip()print("Stripped bytes:", foo_stripped)
终端运行,输出如下:
2.6.4 避免循环中的临时容器创建
在循环中频繁创建临时容器(如 for _ in range(100): temp = [1,2,3])会创建大量临时对象,占用 RAM。优化思路是将临时容器移到循环外,复用对象,或使用生成器 / 迭代器代替临时列表。
import gcimport struct# 初始化垃圾回收,建立测试基线gc.collect()ram_init = gc.mem_free()# 定义循环次数(模拟多次迭代场景)loop_times = 100# ---------------------- 低效实现:循环内创建临时列表 ----------------------# 循环内每次创建新的列表容器,产生大量临时对象gc.collect()ram_slow_before = gc.mem_free()total_slow = 0# 每次循环创建新列表[1,2,3,4,5]# struct测试列表数据的字节大小(打包为5个整数,用'i'格式)for _ inrange(loop_times): temp_list = [1, 2, 3, 4, 5] total_slow += sum(temp_list) size_slow = len(struct.pack("5i", *temp_list))# 记录低效实现后的RAMram_slow_after = gc.mem_free()# ---------------------- 高效实现1:循环外创建列表,复用容器 ----------------------# 列表仅创建一次,循环内复用gc.collect()ram_fast1_before = gc.mem_free()total_fast1 = 0# 临时列表移到循环外(仅创建1次)# struct测试复用列表的字节大小(与低效实现一致)temp_list_reuse = [1, 2, 3, 4, 5]for _ inrange(loop_times): total_fast1 += sum(temp_list_reuse) size_fast1 = len(struct.pack("5i", *temp_list_reuse))# 记录高效实现1后的RAMram_fast1_after = gc.mem_free()# ---------------------- 高效实现2:使用生成器,替代临时列表 ----------------------# 定义生成器函数(仅创建一次,循环内复用迭代器)# 直接yield元素,避免元组创建defnum_generator():yield1yield2yield3yield4yield5# 生成器不创建完整列表,仅迭代生成元素,内存开销最低gc.collect()ram_fast2_before = gc.mem_free()total_fast2 = 0# 生成器表达式(无临时列表对象)# struct测试生成器数据的字节大小(打包为5个整数)for _ inrange(loop_times): total_fast2 += sum(num_generator()) size_fast2 = len(struct.pack("5i", 1, 2, 3, 4, 5))# 记录高效实现2后的RAMram_fast2_after = gc.mem_free()# 输出测试结果(无中文)print("Slow total:", total_slow)print("Slow RAM change:", ram_slow_before - ram_slow_after)print("Slow data size:", size_slow)print("Fast1 total:", total_fast1)print("Fast1 RAM change:", ram_fast1_before - ram_fast1_after)print("Fast1 data size:", size_fast1)print("Fast2 total:", total_fast2)print("Fast2 RAM change:", ram_fast2_before - ram_fast2_after)print("Fast2 data size:", size_fast2)
终端运行结果如下:
我们可以看到:
- 每次循环创建新列表对象:内存消耗为 10528 字节;
- 循环外创建列表复用容器:内存消耗为 5888 字节;
在小数据量(5 个固定数)重复 100 次的场景下,列表复用确实比生成器更省内存;当需要处理大量数据或流式数据时,生成器避免了一次性加载所有数据。
2.6.5 避免运行时编译器执行(eval/exec)
eval()/exec() 会在运行时调用 MicroPython 编译器,创建大量临时对象,消耗大量 RAM;直接计算或使用 json 序列化替代,可减少内存开销。
import gcimport structimport json# ---------------------- 低效:使用eval ----------------------gc.collect()ram_eval_before = gc.mem_free()# 避免使用eval,此处仅作对比res_eval = eval("1 + 2 * 3 + 4 / 2")ram_eval_after = gc.mem_free()# ---------------------- 高效:直接计算 ----------------------gc.collect()ram_calc_before = gc.mem_free()res_calc = 1 + 2 * 3 + 4 / 2ram_calc_after = gc.mem_free()# ---------------------- 高效:ujson序列化 ----------------------gc.collect()ram_json_before = gc.mem_free()# 序列化data = {"temp": 25.5, "press": 101325}json_data = json.dumps(data)# 反序列化size_json = len(struct.pack(f"{len(json_data.encode())}s", json_data.encode()))data_loaded = json.loads(json_data)ram_json_after = gc.mem_free()# 输出测试结果print("Eval RAM change:", ram_eval_before - ram_eval_after)print("Calc RAM change:", ram_calc_before - ram_calc_after)print("JSON size:", size_json)print("JSON RAM change:", ram_json_before - ram_json_after)
终端运行结果如下:
我们可以看到,动态解析(eval())和 JSON 处理会创建大量隐藏的临时对象:
eval("1+2*3+4/2") 虽然结果相同但消耗 592 字节内存,比直接计算(384 字节)多出 208 字节;- 而 JSON 序列化 31 字节数据却消耗 1488 字节内存,因为解析字符串、创建中间字典、转换浮点数等每个步骤都产生碎片化分配,最终实际内存开销是数据大小的 48 倍。
这正是微控制器上内存碎片化的典型表现——表面简单的操作背后隐藏着指数级的内存消耗。
2.6.6 在闪存中存储字符串(qstr 机制)
MicroPython 的 qstr 量化字符串机制会将重复的字符串存储在闪存中,而非 RAM,减少 RAM 占用,我们可以通过 micropython.qstr_info() 查看字符串存储状态,用 struct 测试字符串字节大小,用 gc 查看 RAM 变化。
import gcimport structimport micropython# 初始化垃圾回收gc.collect()ram_init = gc.mem_free()# 定义测试字符串(会被qstr机制处理)s1 = "MicroPython"# 重复字符串,复用闪存中的qstrs2 = "MicroPython"# struct测试字符串字节大小size_s = len(struct.pack(f"{len(s1.encode())}s", s1.encode()))# 打印qstr信息(1表示详细输出)micropython.qstr_info(1)# 记录RAMram_after = gc.mem_free()# 输出测试结果print("String size:", size_s)print("RAM free:", ram_after)
终端输出结果如下:
我们可以看到,s1 和 s2 两个字符串内容相同,都是 "MicroPython",MicroPython 的 qstr 机制会重用相同字符串,实际上 s1 和 s2 指向同一个内存中的字符串对象,同时节省了重复存储相同字符串的内存。
qstr 信息分析如下:
>>> micropython.qstr_info(1)qstr pool: n_pool=1, n_qstr=5, n_str_data_bytes=26, n_total_bytes=170Q(ram_init)Q(s1)Q(s2)Q(size_s)Q({}s)
关键信息解析:
n_pool=1:有 1 个 qstr 池;
n_qstr=5:当前有 5 个驻留的字符串;
n_str_data_bytes=26:字符串数据总大小 26 字节;
n_total_bytes=170:qstr 系统总占用 170 字节(qstr 管理结构本身有约 144 字节的开销)。
并且列出的 5 个 qstr:
ram_init:变量名(8 字符);
s1:变量名(2 字符);
s2:变量名(2 字符);
size_s:变量名(5 字符);
{}s:format 字符串的一部分(3 字符)。
但是,"MicroPython" 这个字符串没有在列出的 qstr 中,是因为 MicroPython 将它作为只读数据存储在 Flash 中,而不是 RAM 中。
2.7 堆与垃圾回收机制
堆是一块动态分配的内存区域,用于存储程序运行时创建的对象实例和动态数据结构(比如 Python 中的列表、字典、自定义类的实例):
- 堆的大小不是固定的,会根据程序需求动态扩展(受限于系统总内存);
- 在 Python、Java 等语言中,堆的分配是由语言的虚拟机(VM)自动完成的,开发者无需手动申请内存。
当你执行 lst = [1,2,3] 时,Python 会:
- 在堆中分配一块内存,存储列表
[1,2,3] 的对象数据; - 在栈中分配一个变量
lst,存储的是堆中该对象的内存地址(引用)。也就是说,变量 lst 只是一个「指针」,指向堆中的实际数据。
堆中的对象如果没有任何引用指向它(即程序无法再访问到该对象),这个对象就成为「垃圾」:例如执行 lst = None 后,原本堆中的 [1,2,3] 对象就失去了引用,变成垃圾。
MicroPython 中 GC 垃圾回收可以自动识别并清理堆中的垃圾对象,释放占用的内存,避免内存泄漏(内存被无用对象一直占用,导致程序可用内存越来越少,最终崩溃)。
垃圾回收的触发方式包括以下两种:
- 自动触发:当堆内存占用达到阈值、或对象引用计数为 0 时,语言引擎自动触发 GC。
- 手动触发:开发者可以通过代码主动调用 GC(比如 Python 中的
gc.collect())。
MicroPython 的 gc 模块有下面的常用的核心方法:
下面是我们的测试代码:
# 导入gc模块,用于操作垃圾回收(mpy版)import gc# 1. 查看自动GC是否开启(部分mpy版本支持)print(gc.isenabled())# 2. 禁用自动GC,方便手动控制测试过程gc.disable()print(gc.isenabled())# 3. 查看初始的空闲堆内存和已分配堆内存(mpy专属方法)print(f"Free heap: {gc.mem_free()} bytes")print(f"Allocated heap: {gc.mem_alloc()} bytes")# 4. 演示gc.threshold()方法:设置并查询GC分配阈值(mpy专属方法)# 设置阈值为1024字节,累计分配1024字节后触发GC(提前回收减少碎片)gc.threshold(1024)# 无参数时查询当前阈值current_threshold = gc.threshold() print(f"Current GC threshold: {current_threshold} bytes")# 5. 创建普通对象,占用堆内存(变量是栈中的引用,指向堆对象)# 创建更大的列表,内存变化更明显obj1 = [1, 2, 3, 4, 5] obj2 = {"name": "test", "age": 18}print("Objects created")# 6. 查看创建对象后的内存变化print(f"Free heap after create: {gc.mem_free()} bytes")print(f"Allocated heap after create: {gc.mem_alloc()} bytes")# 7. 切断引用,让对象成为垃圾(堆中对象无引用指向)obj1 = Noneobj2 = Noneprint("References cut, objects become garbage")# 8. 查看切断引用后的内存(未GC,内存未释放)print(f"Free heap before collect: {gc.mem_free()} bytes")print(f"Allocated heap before collect: {gc.mem_alloc()} bytes")# 9. 手动触发GC,清理垃圾对象# mpy中部分版本无返回值,故移除接收变量gc.collect() print("Garbage collection executed")# 10. 查看GC后的内存变化(垃圾被清理,空闲内存增加)print(f"Free heap after collect: {gc.mem_free()} bytes")print(f"Allocated heap after collect: {gc.mem_alloc()} bytes")# 11. 演示循环引用的垃圾回收(引用计数法无法处理,需gc.collect())a = []b = []# a引用b, b引用a,形成循环引用a.append(b) b.append(a) print("Circular reference objects created")# 12. 查看创建循环引用对象后的内存print(f"Free heap after circular ref: {gc.mem_free()} bytes")print(f"Allocated heap after circular ref: {gc.mem_alloc()} bytes")# 13. 切断外部引用,此时a和b互相引用,引用计数不为0a = Noneb = Noneprint("External references cut (circular ref remains)")# 14. 手动触发GC,清理循环引用的垃圾gc.collect()print("Garbage collection for circular ref executed")# 15. 查看清理循环引用后的内存print(f"Free heap after circular collect: {gc.mem_free()} bytes")print(f"Allocated heap after circular collect: {gc.mem_alloc()} bytes")# 16. 恢复自动GC,并重置阈值为默认(-1表示禁用阈值)gc.threshold(-1)print(f"Reset GC threshold: {gc.threshold()}")gc.enable()print(gc.isenabled())
运行终端,输出如下:
整体流程如下图所示:
内存变化如下表所示:
我们也可以使用 micropython.mem_info(1) 方法查看堆利用率表:
这是 MicroPython 的内存信息详细输出,用于查看栈和堆的整体状态,是分析内存的基础:
stack:516outof7936GC: total:233024,used:7168,free:225856No. of 1-blocks:96,2-blocks:26,max blk sz:64,max free sz:14075...(内存布局省略)
stack: 516 out of 7936 :栈内存已使用 516 字节,总大小 7936 字节。栈内存用于存储局部变量、函数调用上下文,这里栈使用量很小,处于安全状态。
GC: total: 233024, used: 7168, free: 225856:堆内存的核心统计(单位:字节):
totalused:当前已分配的堆内存(7168 字节),即程序中对象占用的堆空间。free:当前空闲的堆内存(225856 字节),可用于分配新对象。
块信息(1-blocks/2-blocks 等):MicroPython 的堆内存以「块」为单位分配,这里显示了不同大小块的数量、最大块大小、最大空闲块大小,反映了堆的碎片化程度。
内存布局:以字符形式展示堆内存的使用状态(如 h 表示已用块、= 表示空闲块、D/B/S 是特殊标记),后续的 219 lines all free 表示大部分堆内存都是空闲的。
参考资料
https://docs.micropython.org/en/latest/reference/index.html