ESP32 工程实践:用三个堆内存指标排查泄漏与碎片化
网页响应、JSON、图像和网络缓冲都会占用堆内存。程序运行一段时间后分配失败,不能只看“还剩多少字节”:单次申请通常还需要足够大的连续可用区域,而且不同外设对内存能力有不同要求。
示例面向 Arduino-ESP32 3.x,调用 ESP-IDF 的堆能力接口;未完成实机验证。本文关注动态堆内存,不把它等同于任务栈或所有物理 RAM。
三个指标分别回答什么
| 指标 | 回答的问题 |
|---|---|
| 当前空闲总量 | 当前该类堆还有多少可分配空间 |
| 最大连续空闲块 | 单次分配可能取得多大的连续区域 |
| 历史最低空闲量 | 从启动以来该类堆曾经低到什么程度 |
空闲总量较多但最大块很小,可能与碎片化、多个内存区域或分配约束有关。历史最低值下降也不一定是泄漏,首次 TLS 握手或首次加载页面本来就可能形成新的峰值。
每五秒采样一次
#include <Arduino.h>
#include <esp_heap_caps.h>
void reportHeap(const char* label, uint32_t caps) {
Serial.printf("%s free=%lu largest=%lu minimum=%lu\n", label,
(unsigned long)heap_caps_get_free_size(caps),
(unsigned long)heap_caps_get_largest_free_block(caps),
(unsigned long)heap_caps_get_minimum_free_size(caps));
}
void setup() {
Serial.begin(115200);
}
void loop() {
static uint32_t last = 0;
const uint32_t now = millis();
if (uint32_t(now - last) >= 5000) {
last = now;
reportHeap("internal-8bit", MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
reportHeap("psram-8bit", MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
delay(10);
}
没有启用或没有配置 PSRAM 的板卡,PSRAM 指标可以为零。两类指标分开观察,不要把不同能力集合的统计随意相加:某些能力集合会重叠。
这些调用是多个独立快照,并发任务可能在调用之间分配或释放内存。堆的历史最低统计还涉及各区域的低水位,不应视为精确记录的某一时刻系统总空闲值。适合比较趋势,不能替代完整的分配追踪。
用重复操作区别问题
选择一个可以反复执行的业务动作,例如打开状态页。先完成一次初始化和预热,然后执行多轮相同操作,在连接关闭、资源清理完成后采样。
如果空闲总量持续下降,应查找未释放的对象、未关闭的会话、无上限容器和异常返回分支。若总量大致恢复但最大块持续缩小,应关注变长缓冲的分配模式。若指标趋于稳定,则可能只是库缓存;还要结合更长时间和失败场景验证。
不要把一组人为编造的数字当作实测结果。建议记录时间、业务动作、连接数、固件版本和三个指标,保留可比较的原始日志。
改进分配方式
对长度有明确上限的消息复用缓冲,校验输入长度,再复制或解析。频繁拼接字符串时,可对预期容量使用 String.reserve() 并检查返回值,但预留本身会占内存,不能无限放大。
摄像头、DMA 和驱动缓冲应遵循对应接口要求。存在 PSRAM 不代表所有分配或外设都能使用它;需要显式能力分配时,按芯片及驱动约束选择 heap_caps_malloc() 的标志,并检查返回指针。
若怀疑越界或释放后访问,可在调试构建启用 ESP-IDF 堆调试能力。生产现场出现内存不足时应停止非必要工作并释放可回收资源,避免拿空指针继续运行。
浙公网安备 33010602011771号