从 RAM 到 Flash:MicroPython 嵌入式内存优化的底层逻辑与实践
1.前置概念MicroPython 的核心应用场景是微控制器如树莓派 Pico、ESP32、ESP8266这类设备的硬件资源与电脑 / 服务器存在数量级的差距 —— 通常 RAM运行内存只有几十 KB 到几百 KBFlash存储内存也仅几 MB。而 Python 代码本身具有动态性运行时会占用一定的内存资源因此在微控制器上使用 MicroPython 时内存优化是保证程序稳定运行的关键。本文将详细介绍 MicroPython 内存优化的方法其中会对新手不易理解的专有名词进行标注解释同时为核心知识点提供可在 REPLMicroPython 交互式命令行中运行的测试代码帮助你边学边验证。在学习具体优化方法前先理解以下核心专有名词能让后续内容更易理解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树莓派 pico 使用 spi 操作 sd 卡 https://www.bilibili.com/video/av205357901/?vd_sourceccb41f7ce2bc22dbc99b62c49f7d8d0d使用 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/131592815MicroPython 设置忽略 SD 卡启动 https://pico.nxez.com/2018/12/21/micropython-settings-ignore-sd-card-boot.html2.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 生成冻结模块和字节码的步骤一个生成冻结模块和字节码的步骤通常如下克隆 MicroPython 源代码存储库获取特定于平台的工具链以构建固件构建交叉编译器将要冻结的模块放在指定目录中取决于模块是作为源还是字节码冻结构建固件。可能需要特定命令来构建任一类型的冻结代码将固件刷入设备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/555Frozen Modules https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modulesHow to freeze module for ESP32 https://forum.micropython.org/viewtopic.php?t122682.4 使用.mpy 文件将 python 模块预编译为字节码也称为 .mpy 文件MicroPython 的预编译字节码文件格式区别于标准 Python 的.pyc 文件然后将其复制到开发板上。这样做的优点是可以跳过 开发板上的预编译阶段从而避免在此过程中没有 RAM 资源。不幸的是这种方法仍然需要开发板将模块加载到 RAM 中。将 python 模块转换为 .c 文件该文件被编译到固件本身中。具有上述方法的优点 具有从闪存运行模块而不是将其加载到 RAM 中的优点。.mpy 文件生成与使用步骤可以参考如何加快 MicroPython 的运行速度2.5 使用 const 常量2.5.1MicroPython 命名空间和作用域MicroPython 中所有加载到 RAM 的模块都会放在sys.modulessys.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会创建局部变量 # 闭包外层无法访问内部局部变量 def scope_demo(): enclosing_var I am an enclosing variable (outer of closure) def inner_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修改全局变量 def modify_global(): global global_var global_var Modified global variable print(Inside function:, global_var) # 多层嵌套中的nonlocal使用 # 内部middle()函数和inner()函数中修改嵌套作用域中的变量outer_var def nonlocal_demo(): outer_var Outer layer def middle(): nonlocal outer_var outer_var Modified by middle layer def inner(): 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 builtins print(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(fInitial 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 ROWS b _COLS print(fCONST_X: {CONST_X}, CONST_Y: {CONST_Y}) print(fa: {a}, b: {b}) # 第四步查看定义常量后的内存使用情况 gc.collect() after_free gc.mem_free() print(fFree RAM after defining constants: {after_free} bytes) print(fRAM change: {after_free - initial_free} bytes (positive means memory freed)) # 补充对比普通变量与const常量的区别 # 普通变量存储在RAM中 normal_var 123 gc.collect() after_normal gc.mem_free() print(fFree RAM after defining normal variable: {after_normal} bytes) print(fNormal variable uses {after_free - after_normal} more bytes than const)ROWS 值将占用至少两个机器字全局字典中的键和值各一个。字典中的存在是必要的因为另一个模块可能会导入或使用它。可以通过在名称前面添加下划线来保存此 RAM如 _COLS 所示此符号在模块外部不可见因此不会占用 RAM。终端运行结果如下const () 的参数必须是任何在编译时计算结果为整数的值。from micropython import const ROWS const(33) # 下面用法报错 COLS const(0x10ROWS) # 正确用法 COLS const(0x1033)终端输出如下2.6 减少不必要的对象创建在 MicroPython 中对象如字符串、列表、字典、字节数组、数值容器等的创建会占用 RAM资源。由于微控制器的 RAM 容量极小频繁创建和销毁对象不仅会直接消耗内存还会导致内存碎片化即 RAM 中出现大量小的空闲内存块无法被大对象使用最终引发内存不足的问题。需要注意的是标准 Python 中的sys.getsizeof()函数在 MicroPython 中大部分平台不支持这是 MicroPython 的特性为了精简固件因此我们可以通过struct模块将变量 / 数据打包为字节流再通过len()函数获取字节流的长度以此来测试数据占用的字节大小同时结合gc模块垃圾回收查看 RAM 的实际使用变化完成优化前后的对比。2.6.1 字符串操作优化避免临时字符串对象MicroPython 中的字符串是不可变对象使用拼接字符串时每一次都会创建一个临时字符串对象比如abc会先创建ab再创建abc共 2 个临时对象这些临时对象会占用额外的 RAM且会被垃圾回收器频繁清理影响性能。优化思路使用字符串直接拼接编译时合并无临时对象使用join()方法仅创建一个最终字符串对象使用多行字符串或编译时合并。# 静态字符串拼接编译时合并无临时对象 static_str MicroPython Memory Opt print(Static string:, static_str) # 动态字符串推荐format减少临时对象 temp 25.5 press 101325 dynamic_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 # 初始化UART uart UART(0, baudrate9600, txPin(0), rxPin(1)) # 预分配缓冲区仅创建1次 uart_buf bytearray(16) # 循环接收复用缓冲区 for _ in range(5): uart.readinto(uart_buf) print(UART data:, uart_buf)下面我们预分配 SPI 读写缓冲区复用减少内存开销。from machine import SPI, Pin # 初始化SPI spi SPI(0, baudrate1000000, sckPin(2), mosiPin(3), misoPin(4)) # 预分配缓冲区 spi_buf bytearray(8) # 循环读写复用缓冲区 for _ in range(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 bthe quick brown fox print(String:, s) print(Bytes:, b) # 2. 字符串与字节的转换注意转换会创建新对象 # str - bytes s_to_b s.encode() # bytes - str b_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 gc import 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 _ in range(loop_times): temp_list [1, 2, 3, 4, 5] total_slow sum(temp_list) size_slow len(struct.pack(5i, *temp_list)) # 记录低效实现后的RAM ram_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 _ in range(loop_times): total_fast1 sum(temp_list_reuse) size_fast1 len(struct.pack(5i, *temp_list_reuse)) # 记录高效实现1后的RAM ram_fast1_after gc.mem_free() # ---------------------- 高效实现2使用生成器替代临时列表 ---------------------- # 定义生成器函数仅创建一次循环内复用迭代器 # 直接yield元素避免元组创建 def num_generator(): yield 1 yield 2 yield 3 yield 4 yield 5 # 生成器不创建完整列表仅迭代生成元素内存开销最低 gc.collect() ram_fast2_before gc.mem_free() total_fast2 0 # 生成器表达式无临时列表对象 # struct测试生成器数据的字节大小打包为5个整数 for _ in range(loop_times): total_fast2 sum(num_generator()) size_fast2 len(struct.pack(5i, 1, 2, 3, 4, 5)) # 记录高效实现2后的RAM ram_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 字节使用生成器内存消耗为 8720 字节。在小数据量5 个固定数重复 100 次的场景下列表复用确实比生成器更省内存当需要处理大量数据或流式数据时生成器避免了一次性加载所有数据。2.6.5 避免运行时编译器执行eval/execeval()/exec()会在运行时调用 MicroPython 编译器创建大量临时对象消耗大量 RAM直接计算或使用json序列化替代可减少内存开销。import gc import struct import 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 / 2 ram_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(12*34/2)虽然结果相同但消耗 592 字节内存比直接计算384 字节多出 208 字节而 JSON 序列化 31 字节数据却消耗 1488 字节内存因为解析字符串、创建中间字典、转换浮点数等每个步骤都产生碎片化分配最终实际内存开销是数据大小的 48 倍。这正是微控制器上内存碎片化的典型表现——表面简单的操作背后隐藏着指数级的内存消耗。2.6.6 在闪存中存储字符串qstr机制MicroPython 的qstr量化字符串机制会将重复的字符串存储在闪存中而非 RAM减少 RAM 占用我们可以通过micropython.qstr_info()查看字符串存储状态用struct测试字符串字节大小用gc查看 RAM 变化。import gc import struct import micropython # 初始化垃圾回收 gc.collect() ram_init gc.mem_free() # 定义测试字符串会被qstr机制处理 s1 MicroPython # 重复字符串复用闪存中的qstr s2 MicroPython # struct测试字符串字节大小 size_s len(struct.pack(f{len(s1.encode())}s, s1.encode())) # 打印qstr信息1表示详细输出 micropython.qstr_info(1) # 记录RAM ram_after gc.mem_free() # 输出测试结果 print(String size:, size_s) print(RAM free:, ram_after)终端输出结果如下我们可以看到s1 和 s2 两个字符串内容相同都是MicroPythonMicroPython 的 qstr 机制会重用相同字符串实际上s1和s2指向同一个内存中的字符串对象同时节省了重复存储相同字符串的内存。qstr信息分析如下 micropython.qstr_info(1) qstr pool: n_pool1, n_qstr5, n_str_data_bytes26, n_total_bytes170 Q(ram_init) Q(s1) Q(s2) Q(size_s) Q({}s)关键信息解析n_pool1有 1 个 qstr 池n_qstr5当前有 5 个驻留的字符串n_str_data_bytes26字符串数据总大小 26 字节n_total_bytes170qstr 系统总占用 170 字节qstr 管理结构本身有约 144 字节的开销。并且列出的 5 个qstrram_init变量名8 字符s1变量名2 字符s2变量名2 字符size_s变量名5 字符{}sformat 字符串的一部分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(fFree heap: {gc.mem_free()} bytes) print(fAllocated heap: {gc.mem_alloc()} bytes) # 4. 演示gc.threshold()方法设置并查询GC分配阈值mpy专属方法 # 设置阈值为1024字节累计分配1024字节后触发GC提前回收减少碎片 gc.threshold(1024) # 无参数时查询当前阈值 current_threshold gc.threshold() print(fCurrent GC threshold: {current_threshold} bytes) # 5. 创建普通对象占用堆内存变量是栈中的引用指向堆对象 # 创建更大的列表内存变化更明显 obj1 [1, 2, 3, 4, 5] obj2 {name: test, age: 18} print(Objects created) # 6. 查看创建对象后的内存变化 print(fFree heap after create: {gc.mem_free()} bytes) print(fAllocated heap after create: {gc.mem_alloc()} bytes) # 7. 切断引用让对象成为垃圾堆中对象无引用指向 obj1 None obj2 None print(References cut, objects become garbage) # 8. 查看切断引用后的内存未GC内存未释放 print(fFree heap before collect: {gc.mem_free()} bytes) print(fAllocated heap before collect: {gc.mem_alloc()} bytes) # 9. 手动触发GC清理垃圾对象 # mpy中部分版本无返回值故移除接收变量 gc.collect() print(Garbage collection executed) # 10. 查看GC后的内存变化垃圾被清理空闲内存增加 print(fFree heap after collect: {gc.mem_free()} bytes) print(fAllocated 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(fFree heap after circular ref: {gc.mem_free()} bytes) print(fAllocated heap after circular ref: {gc.mem_alloc()} bytes) # 13. 切断外部引用此时a和b互相引用引用计数不为0 a None b None print(External references cut (circular ref remains)) # 14. 手动触发GC清理循环引用的垃圾 gc.collect() print(Garbage collection for circular ref executed) # 15. 查看清理循环引用后的内存 print(fFree heap after circular collect: {gc.mem_free()} bytes) print(fAllocated heap after circular collect: {gc.mem_alloc()} bytes) # 16. 恢复自动GC并重置阈值为默认-1表示禁用阈值 gc.threshold(-1) print(fReset GC threshold: {gc.threshold()}) gc.enable() print(gc.isenabled())运行终端输出如下整体流程如下图所示内存变化如下表所示我们也可以使用micropython.mem_info(1)方法查看堆利用率表这是 MicroPython 的内存信息详细输出用于查看栈和堆的整体状态是分析内存的基础stack: 516 out of 7936 GC: total: 233024, used: 7168, free: 225856 No. 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堆内存的核心统计单位字节total堆内存总大小233024 字节。used当前已分配的堆内存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