LuatOS 内存(RAM)使用分析
作者:孟伟 | 最后修改:2026-04-16
一、引言
内存管理是嵌入式开发中的核心课题,尤其在资源受限的物联网设备中显得尤为重要。LuatOS 作为一款专为嵌入式设备设计的操作系统,其内存管理机制直接关系到固件的稳定性与性能表现。本文将从内存基础知识、LuatOS 内核内存分配机制、常见内存问题场景以及内存问题分析方法四个维度,系统性地讲解 LuatOS 的内存管理机制,帮助开发者更好地理解和使用 LuatOS 进行二次开发。
二、内存基础知识
2.1 随机存取存储器(RAM)概述
随机存取存储器(Random Access Memory,简称 RAM)是计算机系统中用于临时存储数据和程序指令的硬件设备。与只读存储器(ROM)不同,RAM 是一种易失性存储器,这意味着当设备断电时,存储在 RAM 中的数据会全部丢失。在嵌入式系统中,RAM 的容量通常非常有限,从几十 KB 到几 MB 不等,这要求开发者在编写代码时必须精打细算,合理规划内存使用。
RAM 的主要作用是为正在运行的程序提供快速的数据访问通道。当我们启动一个程序时,操作系统会将程序代码和数据加载到 RAM 中,CPU 通过直接访问 RAM 来执行指令和处理数据。在 LuatOS 环境中,Lua 虚拟机、用户脚本、运行时数据等都需要占用 RAM 空间。由于 RAM 资源有限且宝贵,开发者需要深入理解内存分配机制,避免内存泄漏和浪费。
从物理层面来看,RAM 由大量的存储单元组成,每个单元可以存储 1 位(bit)的数据。这些存储单元按照行和列的方式组织成二维阵列,通过行地址选通线(Row Address Strobe)和列地址选通线(Column Address Strobe)来定位和访问特定的存储单元。现代 RAM 芯片通常采用同步动态随机存取(SDRAM)技术,能够与系统时钟同步工作,提供更高的数据传输效率。
2.2 Lua 虚拟机内存模型
LuatOS 基于 Lua 脚本语言开发,因此理解 Lua 虚拟机的内存模型对于内存管理至关重要。Lua 虚拟机采用自动内存管理机制,开发者无需手动分配和释放内存,这一特性大大简化了开发流程,但也带来了内存管理的隐性问题。Lua 虚拟机维护着一个内存池,所有 Lua 对象(包括字符串、表、函数、用户数据等)都从这个内存池中分配。
Lua 虚拟机将内存分为三个主要部分: 已分配内存 (allocated memory)、 已使用内存 (used memory)和 碎片内存 (fragmented memory)。已分配内存是 Lua 虚拟机从系统中获取的总内存大小;已使用内存是当前正在被 Lua 对象使用的内存;碎片内存则是已被释放但尚未被重新利用的内存碎片。这三者的关系直接影响了内存的使用效率和可用性。
Lua 的字符串实现采用了独特的 内部化(interning)机制 和不可变性(immutability)策略。每个字符串在内存中都是唯一的,当程序创建相同内容的字符串时,Lua 会复用已有的字符串对象,而不是创建新的副本。这种设计可以有效减少重复字符串的内存占用。然而,字符串拼接操作的实现方式导致了内存效率问题:当拼接两个字符串时,Lua 会分配一块新的内存来存储结果,然后将原字符串的内容复制到新内存中,最后更新引用指针。这意味着每次拼接操作都会产生临时内存分配和复制开销。
Lua 的表(table)是另一种重要的数据类型,既可以作为数组使用,也可以作为字典使用。表在内存中由两个部分组成: 数组部分(array part) 和 哈希部分(hash part) 。数组部分用于存储整数索引的元素,哈希部分用于存储字符串索引或其他非整数键的元素。Lua 虚拟机会根据表的使用情况动态调整这两个部分的大小,但这种调整本身也会产生内存开销。当向表中插入大量元素时,表的内存占用可能会显著增加,尤其是在频繁插入和删除操作的场景下。
三、LuatOS 内存分配机制
3.1 模组内存总体规划
在不同的模组中 ram 也不一样,具体的分配情况也不相同。
以 780ehm 模组为例,该模组具有 8MB RAM 的模组,根据 780ehm 模组的实际测试结果:
I/user.mem.lua 4194296 35752 35752 -- Lua 内存:4MB 总,35KB 已用,35KB 峰值
I/user.mem.sys 3211584 102048 112264 -- 系统内存:约 3.06MB 总,102KB 已用,112KB 峰值
其内存分配大致如下:
| 内存区域 | 大小 | 占比 | 主要用途 |
| Lua 虚拟机内存 | 4MB | 50% | Lua 脚本运行、变量存储、GC 堆 |
| 系统内存 | 3MB | 37.5% | 内核堆、任务栈、中断栈、驱动缓冲区 |
| 保留/隐藏区域 | 1MB | 12.5% | 系统固件保留,专门用于保障核心系统功能的稳定运行与硬件性能 |
这约 1MB 的"隐藏"内存主要分配给了通信协议栈缓存、音频处理缓冲区、WiFi 缓冲区(如适用)、安全引擎缓存和 DMA 描述符等系统组件。这种分配方式体现了 LuatOS 的设计理念:优先保障系统核心功能的稳定运行,然后将剩余资源分配给 Lua 虚拟机。
780EHM 模组的 PSRAM 说明
对于 780EHM 模组来说:
- 物理内存构成:780EHM 使用的是 EC718HM 系列芯片,该系列芯片配备了物理 PSRAM
- 内存区域映射:
sys 内存:实际在 PSRAM 上,与 psram 是同一个东西,数据完全一样
lua 内存:实际在 PSRAM 上,独占一块内存 3. 内存分配关系:
8MB 内存全部位于物理 PSRAM 上
从用户视角看到的 sys/lua/保留区域,只是逻辑上的划分,物理上都在 PSRAM 中
psram 内存区域与 sys 内存区域在 780EHM 上是完全相同的,只是不同的命名方式
不同模组的内存分配差异
需要注意的是,在不同的 BSP 下,LuatOS 固件的内存分配存在巨大差异,而且大小与具体的固件编译配置有关,使用情况又跟具体的库的实现有关系。
3.2 内存查询接口详解
在 LuatOS 中,rtos.meminfo() 是最核心的内存查询接口,用于获取不同类型内存的使用情况:
rtos.meminfo(type)
功能
获取 LuatOS 内存信息。不同芯片平台、固件编译配置下的内存分配和使用方式存在显著差异。
内存区域与物理内存关系
首先需要理解物理内存和逻辑内存的区分:
-
物理内存类型(用大写的 SRAM/PSRAM 代表物理内存):
-
SRAM:静态随机存取存储器,访问速度快但容量较小;一定存在, 但不一定会暴露给客户使用
- PSRAM:外接伪静态随机存取存储器,访问速度较慢但容量较大;不一定存在, 也不一定会启用给客户使用
- 注意 区分 SRAM 和 PSRAM 的差异, 单从硬件上说, SRAM 更快但小, PSRAM 慢但更大.
-
逻辑内存类型(用户可见), sys/psram/lua 是 luatos 暴露给用户展示的内存分配布局:
-
虚拟机内存("lua"):Lua 虚拟机使用的内存区域,包括 Lua 脚本中的变量、函数、表、字符串等
- 系统内存("sys"):系统级内存区域,用于 FreeRTOS 任务栈、驱动缓冲区等
- psram 内存("psram"):专门管理的 PSRAM 区域,用于大容量数据缓存
注意:逻辑内存与物理内存的对应关系因平台而异!
不同平台的差异:
| 平台系列 | lua 区域 | sys 区域 | psram 区域 | 说明 |
| Air780EPM/EHM/Air8000 | 在PSRAM上 | 在PSRAM上 | 在PSRAM上 | sys和psram指向同一PSRAM,只是统计角度不同 |
| Air780E/EG | 静态分配(通常SRAM) | 在SRAM上 | 不存在 | 不支持PSRAM,lua内存固定大小 |
| Air780EP/EPV | 可能SRAM或PSRAM | 在SRAM上 | 可选(需开启PSRAM支持) | 云编译固件可启用PSRAM |
| Air8101 | 在PSRAM上 | 在SRAM上 | 在PSRAM上 | lua从psram分配但不计入统计 |
| PC模拟器 | 独立内存 | 模拟SRAM | 模拟PSRAM | 仅用于开发调试 |
各区域主要功能
-
虚拟机内存("lua"):
-
Lua 脚本执行环境
- Lua 变量、函数、表、字符串等动态分配
- 由 Lua 垃圾回收器管理
- 开发关注重点:避免内存泄漏,控制内存使用峰值
-
系统内存("sys"):
-
FreeRTOS 内核堆(Heap)
- 任务栈、中断栈
- 驱动缓冲区(GPIO、UART、I2C 等)
- 系统级任务:任务调度、中断处理、消息队列等
- 用户涉及有限:主要通过 zbuff 接口间接使用
-
PSRAM 内存("psram"):
-
大容量数据缓存(图像/音频处理)
- 网络通信大文件接收(TCP/UDP/OTA)
- zbuff 缓冲区分配
- 注意:并非所有平台都支持 PSRAM
参数
type
参数含义:"sys"系统内存, "lua"虚拟机内存,"psram"psram内存, 默认为lua虚拟机内存;
数据类型:string;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:sys;
返回值
local total_lua, used_lua, max_used_lua = rtos.meminfo("lua")
total_lua
含义说明:总内存大小,单位字节;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:4194296;
used_lua
含义说明:当前已使用的内存大小,单位字节;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:16424;
max_used_lua
含义说明:历史最高已使用的内存大小,单位字节;
数据类型:number;
取值范围:暂无;
注意事项:暂无;
返回示例:16424;
使用示例:
-- rtos.meminfo() 接口使用示例
-- 1. 查询 Lua 虚拟机内存(默认)
local total_lua, used_lua, max_used_lua = rtos.meminfo("lua")
log.info("Lua Memory:",
"Total:", total_lua / 1024, "KB",
"Used:", used_lua / 1024, "KB",
"Peak:", max_used_lua / 1024, "KB")
-- 2. 查询系统内存
local total_sys, used_sys, max_used_sys = rtos.meminfo("sys")
log.info("System Memory:",
"Total:", total_sys / 1024, "KB",
"Used:", used_sys / 1024, "KB",
"Peak:", max_used_sys / 1024, "KB")
-- 3. 查询 PSRAM 内存(如果支持)
local total_psram, used_psram, max_used_psram = rtos.meminfo("psram")
if total_psram and total_psram > 0 then
log.info("PSRAM Memory:",
"Total:", total_psram / 1024, "KB",
"Used:", used_psram / 1024, "KB",
"Peak:", max_used_psram / 1024, "KB")
end
四 Lua 垃圾回收深度解析
本章节介绍是 lua 虚拟机内存回收原理。
4.1 垃圾回收工作原理
Lua 采用 增量标记-扫描(Incremental Mark-and-Sweep) 算法进行垃圾回收,这是一种高效的自动内存管理机制。该算法的工作过程分为两个主要阶段:
- 标记阶段(Mark Phase):从根对象(全局变量、活跃的局部变量、寄存器等)开始,遍历所有可达对象,将其标记为"存活"状态。
- 扫描阶段(Sweep Phase):遍历所有对象,回收未被标记的对象,将其占用的内存标记为可用。
Lua 的增量垃圾回收将标记和扫描过程分解为多个小步骤,与程序执行交替进行。这种设计可以减少垃圾回收对程序响应时间的影响,特别适合实时性要求较高的嵌入式系统。
4.1.1 垃圾回收示例与图解
下面通过一个具体的 Lua 脚本示例,结合标记图解来详细讲解垃圾回收的过程:
-- Lua 垃圾回收示例
log.info("初始内存使用:", collectgarbage("count"), "KB")
-- 创建一些对象
local function create_objects()
-- 创建根对象:全局变量(可达)
_G.global_table = {}
-- 创建局部变量:活跃的局部变量(可达)
local local_table = {}
-- 创建嵌套对象
_G.global_table.nested = {}
local_table.nested = {}
-- 创建循环引用对象(可达)
local cycle1 = {}
local cycle2 = {}
cycle1.ref = cycle2
cycle2.ref = cycle1
-- 创建不可达对象链
local unreachable = {}
unreachable.next = {}
unreachable.next.next = {}
log.info("创建对象后内存使用:", collectgarbage("count"), "KB")
-- 返回局部变量,使其成为根对象
return local_table, cycle1
end
-- 执行对象创建
local active_table, active_cycle = create_objects()
-- 现在:
- _G.global_table :全局变量,直接被_G引用,是根对象,处于存活状态。
-- local_table (返回后为active_table):局部变量,通过返回值被外部引用,是根对象,处于存活状态。
-- _G.global_table.nested :嵌套对象,被_G.global_table引用,可达,处于存活状态。
-- local_table.nested (返回后为active_table.nested):嵌套对象,被local_table引用,可达,处于存活状态。
-- cycle1 (返回后为active_cycle):局部变量,通过返回值被外部引用,与cycle2循环引用,是根对象,处于存活状态。
-- cycle2 :局部变量,与cycle1循环引用,通过active_cycle可达,处于存活状态。
-- unreachable :局部变量,函数返回后无外部引用,不可达,成为垃圾。
-- unreachable.next :嵌套对象,被unreachable引用,不可达,成为垃圾。
-- unreachable.next.next :嵌套对象,被unreachable.next引用,不可达,成为垃圾。
log.info("执行后内存使用:", collectgarbage("count"), "KB")
-- 手动触发垃圾回收
collectgarbage("collect")
log.info("垃圾回收后内存使用:", collectgarbage("count"), "KB")

4.1.2 垃圾回收标记图解
逐行执行状态:
**create_objects函数执行**
**_G.global_table = {}**
--创建全局变量,根对象,存活
**local local_table = {}**
--创建局部变量,存活
**第3-4行:创建嵌套对象**
-- _G.global_table.nested:存活
-- local_table.nested:存活
**第5-8行:创建循环引用对象**
--cycle1、cycle2:存活,相互循环引用
**第9-11行:创建不可达对象链**
-- unreachable:存活
-- unreachable.next:存活
-- unreachable.next.next:存活,形成链表
**第12行:内存记录**
-- 打印创建对象后内存使用
**第13行:return local_table, cycle1**
-- local_table → active_table:外部变量,根对象,存活
-- cycle1 → active_cycle:外部变量,根对象,存活
-- unreachable:及其链表:无外部引用,不可达,成为垃圾
**函数执行后内存记录**
log.info("执行后内存使用:", collectgarbage("count"), "KB")
--打印函数执行后内存使用
**手动触发垃圾回收**
collectgarbage("collect")
-- 标记阶段:标记根对象及其可达对象
-- 扫描阶段:回收unreachable及其链表
**垃圾回收后内存记录**
log.info("垃圾回收后内存使用:", collectgarbage("count"), "KB")
-- 打印垃圾回收后内存使用,内存降低
初始状态:
根对象集:
├── _G.global_table (全局变量)
├── active_table (局部变量)
└── active_cycle (局部变量)
对象图:
_G.global_table → nested
active_table → nested
active_cycle ↔ cycle2 (循环引用)
unreachable → next → next → nil (不可达)
标记阶段(Mark Phase):
- 步骤 1:标记根对象
_G.global_table (标记为存活)
active_table (标记为存活)
active_cycle (标记为存活)
unreachable (未标记)
- 步骤 2:遍历标记可达对象
_G.global_table → nested (标记为存活)
active_table → nested (标记为存活)
active_cycle ↔ cycle2 (标记为存活)
unreachable → next → next → nil (未标记)
扫描阶段(Sweep Phase):
- 遍历所有对象,回收未标记的对象
_G.global_table → nested (保留)
active_table → nested (保留)
active_cycle ↔ cycle2 (保留)
unreachable → next → next → nil (回收)
4.1.3 增量垃圾回收过程
增量垃圾回收将上述过程分解为多个小步骤,与程序执行交替进行:
- 程序执行:创建对象、修改引用关系
- 垃圾回收步骤 1:标记一部分根对象
- 程序继续执行
- 垃圾回收步骤 2:遍历标记一部分可达对象
- 程序继续执行
- 垃圾回收步骤 3:继续标记剩余可达对象
- 程序继续执行
- 垃圾回收步骤 4:扫描一部分内存,回收未标记对象
- 程序继续执行
- 垃圾回收步骤 5:完成剩余内存的扫描和回收
这种交替执行的方式避免了长时间暂停程序执行,适合实时性要求较高的嵌入式系统。
4.2 垃圾回收控制参数
Lua 垃圾回收器的行为由两个关键参数控制:
-
间歇率(Pause):控制垃圾回收器开始新一轮回收的阈值。默认值为 200,表示当内存使用量达到上次回收时的两倍时,开始新的回收循环。
-
值越小,垃圾回收越频繁,内存占用越低,但 CPU 开销越大
- 值越大,垃圾回收间隔越长,内存占用越高,但 CPU 开销越小
-
步进倍率(Step Multiplier):控制垃圾回收器的工作速度相对于内存分配速度的倍率。默认值为 200,表示垃圾回收器以内存分配速度的两倍工作。
-
值小于 100 时,垃圾回收速度慢于内存分配速度,可能导致内存无限增长
- 值越大,垃圾回收越积极,每次回收的内存越多
4.3 collectgarbage() 函数详解
Lua 提供了 collectgarbage([opt [, arg]]) 函数用于控制自动内存管理,该函数在 LuatOS 中同样可用。以下是其主要用法:
| 选项 | 功能 | 参数 | 返回值 |
| "collect" | 执行一次完整的垃圾回收循环 | 无 | 无 |
| "count" | 返回 Lua 使用的总内存(KB) | 无 | 内存大小(带小数) |
| "restart" | 重启垃圾回收器的自动运行 | 无 | 无 |
| "setpause" | 设置垃圾回收器的间歇率 | 新的间歇率值 | 之前的间歇率 |
| "setstepmul" | 设置垃圾回收器的步进倍率 | 新的步进倍率值 | 之前的步进倍率 |
| "step" | 单步运行垃圾回收器 | 步长大小(0 表示最小步长) | 布尔值(是否结束循环) |
| "stop" | 停止垃圾回收器的自动运行 | 无 | 无 |
4.4 collectgarbage() 核心参数详解
4.4.1 重点参数使用示例
下面通过一个详细的示例,结合日志输出,重点讲解 collectgarbage() 函数的三个核心参数:"collect"、"count" 和 "setpause"。
-- collectgarbage() 核心参数示例
-- 辅助函数:格式化内存输出
local function format_mem(mem)
return string.format("%.2f KB", mem)
end
-- 1. 初始状态:使用 count 参数监控内存
log.info("=== 1. 初始内存状态 ===")
local initial_mem = collectgarbage("count")
log.info("初始内存:", format_mem(initial_mem))
-- 2. 创建大量对象
log.info("=== 2. 创建大量对象 ===")
local function create_objects(count)
local objects = {}
for i = 1, count do
objects[i] = {
id = i,
name = string.rep("object_" .. i, 10),
data = {}
}
end
return objects
end
local all_objects = create_objects(5000)
local after_create_mem = collectgarbage("count")
log.info("创建 5000 个对象后内存:", format_mem(after_create_mem))
log.info("内存增加:", format_mem(after_create_mem - initial_mem))
-- 3. 释放部分对象引用
log.info("=== 3. 释放部分对象引用 ===")
for i = 3001, 5000 do
all_objects[i] = nil
end
local after_release_mem = collectgarbage("count")
log.info("释放 2000 个对象后内存:", format_mem(after_release_mem))
log.info("内存变化:", format_mem(after_release_mem - after_create_mem))
-- 4. 使用 collect 参数手动触发垃圾回收
log.info("=== 4. 使用 collect 参数触发垃圾回收 ===")
--下面手动释放内存
--释放引用≠释放内存 :将对象引用设为 nil 只是断开了引用关系,对象占用的内存不会立即释放
--垃圾回收时机 :Lua默认会自动运行垃圾回收,但可以通过 collectgarbage("collect") 手动触发
--手动GC的意义 :在内存敏感操作前或批量释放对象后手动触发GC,可以立即释放内存,避免内存占用过高
collectgarbage("collect") -- 执行完整的垃圾回收循环
local after_collect_mem = collectgarbage("count")
log.info("手动 GC 后内存:", format_mem(after_collect_mem))
log.info("GC 释放内存:", format_mem(after_release_mem - after_collect_mem))
-- 5. 综合示例:监控内存并手动 GC
log.info("\n=== 6. 综合示例:循环创建和回收 ===")
local max_iterations = 5
for i = 1, max_iterations do
-- 创建对象_
local temp = create_objects(2000)
-- 释放引用
temp = nil
-- 监控内存
local mem_before = collectgarbage("count")
-- 手动 GC
collectgarbage("collect")
local mem_after = collectgarbage("count")
log.info("第", i, "次循环 - GC 前:", format_mem(mem_before), "GC 后:", format_mem(mem_after), "释放:", format_mem(mem_before - mem_after))
end
4.4.2 核心参数详解
"count" 参数
功能:返回 Lua 虚拟机当前使用的总内存,单位为 KB(带小数精度)。
使用场景:
- 监控内存使用趋势
- 调试内存泄漏问题
- 评估垃圾回收效果
日志解析:
=== 1. 初始内存状态 ===
初始内存: 120.50 KB
=== 2. 创建大量对象 ===
创建 5000 个对象后内存: 356.75 KB
内存增加: 236.25 KB
关键说明:
- 返回值包含小数部分,提供精确的内存使用情况
- 包括 Lua VM 管理的所有内存,包括已分配但未使用的内存
- 是监控内存变化的主要工具
"collect" 参数
功能:执行一次完整的垃圾回收循环,包括标记、扫描和清理阶段。
使用场景:
- 手动触发垃圾回收,释放不再使用的内存
- 在内存敏感操作前清理内存
- 测试和调试垃圾回收行为
日志解析:
=== 4. 使用 collect 参数触发垃圾回收 ===
手动 GC 前内存: 356.75 KB
手动 GC 后内存: 220.30 KB
GC 释放内存: 136.45 KB
关键说明:
- 执行完整的垃圾回收流程,释放所有不可达对象
- 阻塞执行,直到垃圾回收完成
- 适用于需要立即释放内存的场景
"setpause" 参数
功能:设置垃圾回收器的间歇率,控制垃圾回收开始的阈值。
工作原理:
- 间歇率默认值为 200,表示当总内存使用量达到上次 GC 后活跃内存的 2 倍时,触发新一轮 GC
-
公式:触发条件 = 当前内存 > (基准内存 × pause / 100)
-
基准内存:每次 GC 完成后更新的活跃内存量
- pause 值:百分比单位,100 表示 100%
使用场景:
- 调整垃圾回收频率,平衡内存使用和 CPU 开销
- 针对不同应用场景优化垃圾回收行为
- 在内存紧张时降低间歇率,提高回收频率
关键说明:
- 间歇率越低,垃圾回收越频繁,内存占用越低,但 CPU 开销越大
- 间歇率越高,垃圾回收间隔越长,内存占用越高,但 CPU 开销越小
-
不同应用场景需要不同的设置:
-
内存敏感应用:设置较低的间歇率(如 100-150)
- CPU 敏感应用:设置较高的间歇率(如 300-500)
- 平衡需求:使用默认值 200
4.4.3 最佳实践
- 内存监控:
sys.timerLoopStart(function()
log.info("mem.lua", rtos.meminfo())
log.info("mem.sys", rtos.meminfo("sys"))
--log.info("mem.psram", rtos.meminfo("psram")) --需要时打开
end, 3000)
- 手动 GC 时机:
-- 在内存敏感操作前执行手动 GC
collectgarbage("collect") -- 清理内存
- 间歇率优化:
-- 根据应用场景调整间歇率
collectgarbage("setpause", 150) -- 更频繁的 GC
--collectgarbage("setpause", 300) -- 较少的 GC,节省 CPU
通过合理使用这三个核心参数,可以有效监控和控制 Lua 虚拟机的内存使用,优化应用程序的性能和稳定性。
五、实际功能内存使用案例分析
为了更好地理解 LuatOS 中三种内存的使用情况,下面结合具体的脚本代码,用 780EHM 来详细分析不同功能在运行过程中如何使用 lua、sys 和 psram 内存。
对于 780EHM 来说,sys 分区和 psram 分区都在 PSRAM 上,实际是同一个东西, 数据完全一样,所以只看 sys 内存即可。
5.1 zbuff 功能内存使用分析
zbuff 是 LuatOS 中用于直接操作二进制内存数据的库,内存组成:lua 对象元数据(小) + C 层数据块(大,如存在 PSRAM 则在 psram 中进行申请,如不存在或失败则在 SRAM 上的 sys 中进行申请)。
-- zbuff 内存使用示例_
log.info("初始内存状态:")
log.info("lua:", rtos.meminfo("lua"))
--对于780EHM来说,sys分区和psram分区都在PSRAM上,实际是同一个东西, 数据完全一样,所以只看sys内存即可。
--对于PSRAM以及SRAM都存在的模组,如存在PSRAM则在psram中进行申请,如不存在或失败则在SRAM上的sys中进行申请,
--所以需要根据具体模组来打印sys分区以及psram分区_
log.info("sys:", rtos.meminfo("sys"))
log.info("psram:", rtos.meminfo("psram"))
-- 1. buff_auto这个Lua对象是从lua内存区域分配的,这个对象指向的1MB字节的内存是从sys内存区域分配的
local buff_auto = zbuff.create(1024 * 1024) -- 申请 1MB 内存
local buff_with_data = zbuff.create(512 * 1024, "initial data") -- 申请 512KB 带初始数据的 zbuff
log.info("创建 zbuff 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 2. 使用 zbuff 读写数据
-- 数据读写操作主要影响实际数据块所在的内存
-- 这里只修改数据内容,不改变内存分配情况
buff_auto:write("Hello zbuff!")
buff_with_data:write("Hello zbuff with initial data!")
log.info("写入数据后内存状态:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 3. 释放 zbuff 内存
-- 调用 free() 立即释放实际数据块
-- Lua 对象元数据仍需等待 GC 回收
buff_auto:free()
buff_with_data:free()
log.info("调用 free() 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 4. 释放 Lua 对象引用
-- 将引用设为 nil,等待 GC 回收 zbuff 对象元数据
buff_auto = nil
buff_with_data = nil
collectgarbage("collect") -- 手动触发 GC
log.info("释放引用并 GC 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))


内存使用分析:
步骤 1:初始内存状态
- lua 内存:4194296 34688 34912(总大小 4MB,已用 34688B,可用 34912B)
-
sys 内存:3211584 104760 105880(总大小 3.2MB,已用 104760B,可用 105880B)
步骤 2:创建 zbuff 后
- 代码操作:创建 1MB 和 512KB 的 zbuff 对象
- 内存变化: lua 内存:4194296 34304 35224(已用略减,可用略增,因触发强制 GC) sys 内存:3211584 1678228 1679316(已用大幅增加约 1.5MB,符合 1MB+512KB 分配)
- 关键现象:日志显示 "trigger force GC",说明创建大内存时自动触发垃圾回收
步骤 3:写入数据后
- 代码操作:向两个 zbuff 写入数据
- 内存变化: lua 内存:4194296 34952 35224(已用略增,因字符串字面量分配) sys 内存:3211584 1678656 1679760(已用略增,因数据内容写入)
- 结论:仅修改数据内容,不改变内存分配结构
步骤 4:调用 free() 后
- 代码操作:释放两个 zbuff 的数据块
- 内存变化: lua 内存:4194296 35104 35224(基本不变,对象元数据仍存在)
sys 内存:3211584 106200 1679912(已用大幅减少,恢复到接近初始状态)
- 结论:立即释放 C 层数据块,Lua 对象元数据仍需等待 GC
步骤 5:释放引用并 GC 后
- 代码操作:设为 nil 并手动触发 GC
- 内存变化: lua 内存:4194296 34320 35488(已用减少,恢复到接近初始状态) sys 内存:3211584 106636 1679912(基本不变,数据块已释放)
- 结论:彻底回收 Lua 对象元数据,内存完全释放
关键结论
zbuff 通过分离 Lua 元数据(小,存储对象信息)和 C 层数据块(大,存储实际数据),实现了高效的内存管理:
- 大内存块在 sys 分区/psram 分区 中分配,不占用 Lua 内存
- 支持手动释放数据块,避免内存泄漏
- 创建大内存时自动触发 GC,优化内存使用
- 适合处理大块二进制数据,如网络数据、文件数据等
5.2 UART 功能内存使用分析
UART 是嵌入式系统常用通信接口,内存组成:lua 配置信息(小) + sys 发送/接收缓冲区(核心)。
-- UART 内存使用示例
log.info("初始内存状态:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
local uart_id = 1 -- UART 端口号
-- 1. 初始化 UART
-- 发送和接收缓冲区在 sys 内存中分配
local setup_result = uart.setup(uart_id, -- 串口 ID
115200, -- 波特率
8, -- 数据位
1, -- 停止位
uart.NONE, -- 校验位(uart.NONE/uart.EVEN/uart.ODD)
uart.LSB, -- 大小端(uart.LSB/uart.MSB)
4096 -- 接收缓冲区大小:4KB,发送缓冲区大小固定
)
log.info("初始化 UART 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 2. 发送数据(字符串方式)
-- 数据首先被复制到 UART 发送缓冲区(sys 内存)
-- 然后由硬件自动发送
local send_data = string.rep("test_data_", 100) -- 约 900 字节的数据
log.info("发送数据前:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 注册发送完成回调
local function uart_sent_callback(id)
log.info("UART 发送完成回调:", id)
-- 发送完成后,发送缓冲区会自动释放
log.info("发送完成后内存状态:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
end
-- 注册发送完成事件回调
uart.on(uart_id, "sent", uart_sent_callback)
-- 数据复制到发送缓冲区的操作在 uart.write() 调用中执行
-- 这一步会将 send_data 字符串复制到 UART 发送缓冲区(sys 内存)
uart.write(uart_id, send_data)
log.info("调用 uart.write() 后(数据已复制到发送缓冲区):")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 等待发送完成回调(实际使用中应在回调中处理,这里为演示暂停)
sys.wait(1000) -- 等待发送完成
-- 3. 发送数据(zbuff 方式)
-- 使用 zbuff 发送数据,减少内存拷贝
local txbuff = zbuff.create(1024)
log.info("创建发送 zbuff 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 向 zbuff 写入数据
local write_len = txbuff:write("Hello from zbuff!")
log.info("使用 zbuff 发送数据:")
-- 使用 uart.tx() 发送 zbuff 数据
-- 此方式直接使用 zbuff 数据,减少一次内存拷贝
local tx_result = uart.tx(uart_id, txbuff)
log.info("调用 uart.tx() 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 等待发送完成回调
sys.wait(1000) -- 等待发送完成
log.info("zbuff 发送完成后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 4. 接收数据回调
-- 接收数据存储在 UART 接收缓冲区(sys 内存)
local rxbuff = zbuff.create(4096)
log.info("创建接收 zbuff 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
local function uart_receive_callback(id, len)
log.info("UART 接收数据回调,可用长度:", len)
-- 从 UART 接收缓冲区读取数据到 zbuff
local read_len = uart.rx(id, rxbuff)
if read_len > 0 then
log.info("实际读取长度:", read_len)
log.info("接收数据:", rxbuff:toStr(0, read_len))
-- 接收数据后,UART 接收缓冲区会自动清空
log.info("接收数据后内存状态:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 重置 zbuff 指针,准备下次接收
rxbuff:seek(0)
end
end
-- 注册接收回调
uart.on(uart_id, "receive", uart_receive_callback)
-- 5. 释放资源
-- 释放 zbuff 资源
txbuff:free()
rxbuff:free()
log.info("释放 zbuff 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 6. 关闭串口
-- 使用 uart.close() 函数关闭串口,释放串口相关资源
uart.close(uart_id)
log.info("关闭串口后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 注意:uart.close() 函数用于关闭串口,释放相关资源
-- 调用后串口将不再可用,需要重新调用 uart.setup() 才能再次使用



内存使用分析:
步骤 1:初始内存状态
lua 内存:4194296 38176 38304(总大小 4MB,已用 38176B,可用 38304B)
sys 内存:3211584 104760 105880(总大小 3.2MB,已用 104760B,可用 105880B)
步骤 2:初始化 UART 后
代码操作:调用 uart.setup() 配置串口参数
内存变化:
结论:初始化过程分配了接收缓冲区和内部数据结构
步骤 3:发送数据前
代码操作:创建发送数据字符串
内存变化:
步骤 4:调用 uart.write() 后(数据已复制到发送缓冲区)
代码操作:注册发送完成回调并调用 uart.write()
内存变化:
关键现象:数据复制到发送缓冲区的操作在 uart.write() 调用中执行
步骤 5:发送完成后(回调中)
代码操作:发送完成回调触发
内存变化:
结论:发送完成后,发送缓冲区自动释放
步骤 6:创建发送 zbuff 后
代码操作:创建 1024B 的发送 zbuff
内存变化:
步骤 7:调用 uart.tx() 后
代码操作:使用 zbuff 发送数据
内存变化:
结论:zbuff 方式发送无额外内存拷贝,效率更高
步骤 8:zbuff 发送完成后
代码操作:等待发送完成
内存变化:
步骤 9:创建接收 zbuff 后
代码操作:创建 4096B 的接收 zbuff
内存变化:
步骤 10:释放 zbuff 后
代码操作:释放发送和接收 zbuff
内存变化:
步骤 11:关闭串口后
代码操作:调用 uart.close() 关闭串口
内存变化:
关键结论
内存分配结构:UART 主要使用 sys 内存作为收发缓冲区,lua 内存仅存储配置对象和回调函数
发送方式对比:
资源管理:
内存优化建议:大数据发送优先使用 zbuff 方式,减少内存拷贝和碎片
5.3 MQTT 功能内存使用分析
MQTT 是物联网设备常用的轻量级通信协议,在 LuatOS 中,MQTT 客户端的内存使用涉及 lua 和 sys 内存分配:
- lua 内存:主要存储客户端对象、配置信息、订阅信息和临时消息对象,占用较小但管理核心逻辑
- sys 内存:主要存储连接和收发缓冲区,是内存使用的核心部分,占用较大且随消息大小动态变化
-- MQTT 内存使用示例
log.info("初始内存状态:")
log.info("lua:", rtos.meminfo("lua")) -- 记录Lua虚拟机内存使用
log.info("sys:", rtos.meminfo("sys")) -- 记录系统内存使用
-- 配置 MQTT 服务器信息
local SERVER_ADDR = "lbsmqtt.airm2m.com"
local SERVER_PORT = 1884
local USERNAME = "test"
local PASSWORD = "test"
local CLIENT_ID = "luatos_test_device"
-- 定义 MQTT 客户端回调函数
local function mqtt_client_cb(mqtt_client, event, data, payload, metas)
log.info("MQTT 事件:", event, data, payload and #payload or "nil")
-- 连接成功事件
if event == "conack" then
log.info("MQTT 连接成功")
-- 连接成功后可以订阅主题
-- 内存影响:
-- - lua内存:增加约592B,存储订阅信息
-- - sys内存:基本不变
mqtt_client:subscribe("device/commands", 0)
-- 对应内存分析:步骤 6,订阅主题后
log.info("订阅主题后内存:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
end
-- 接收到消息事件
if event == "recv" then
log.info("MQTT 接收消息:", data, #payload, "字节")
-- 内存影响:接收到消息时的临时内存使用
-- 对应内存分析:接收消息后,检查消息接收的内存变化
log.info("接收消息后内存:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 对于大消息,可以使用 zbuff 处理
-- 内存影响:创建临时zbuff对象,处理完成后释放
if #payload > 1024 then -- 大于 1KB 的消息
local buff = zbuff.create(#payload) -- 创建对应大小的zbuff
buff:write(payload) -- 写入数据
-- 处理数据...
buff:free() -- 释放zbuff数据块
end
end
end
-- MQTT 客户端需要在 task 中运行
local function mqtt_test_task()
-- 1. 创建 MQTT 客户端
-- 参数:nil, 服务器地址, 服务器端口
-- 内存影响:
-- - lua内存:增加约38120B,存储客户端对象和内部数据结构
-- - sys内存:增加约3848B,分配基础结构
local mqtt_client = mqtt.create(nil, SERVER_ADDR, SERVER_PORT)
-- 对应内存分析:步骤 3,创建 MQTT 客户端后
log.info("创建 MQTT 客户端后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 2. 配置认证信息
-- 内存影响:
-- - lua内存:增加约264B,存储认证配置
-- - sys内存:基本不变
mqtt_client:auth(CLIENT_ID, USERNAME, PASSWORD, true)
-- 对应内存分析:步骤 4,配置认证信息后
log.info("配置认证信息后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 3. 注册事件回调
-- 内存影响:无显著内存变化,仅注册回调函数
mqtt_client:on(mqtt_client_cb)
-- 4. 设置 keepalive
-- 内存影响:无显著内存变化,仅设置参数
mqtt_client:keepalive(30)
-- 5. 连接 MQTT 服务器
-- 内存影响:
-- - lua内存:增加约160B,存储连接状态
-- - sys内存:增加约33204B,分配连接和收发缓冲区
local connect_result = mqtt_client:connect()
-- 对应内存分析:步骤 5,连接 MQTT 服务器后
log.info("连接 MQTT 服务器后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 等待连接成功(最多10秒)
-- 对应内存分析:步骤 2,网络连接就绪,无明显内存变化
local connected = false
local start_time = os.time()
while not connected and os.time() - start_time < 10 do
sys.wait(1000)
-- 检查连接状态(实际应用中通过事件判断)
connected = true -- 简化示例,实际应通过事件回调判断
end
if connected then
-- 6. 发布小消息
-- 内存影响:
-- - lua内存:增加约376B,存储消息对象
-- - sys内存:减少约472B,可能是临时对象释放
local small_msg = "{\"temp\":25.5,\"humidity\":60}" -- 约 30 字节
-- 对应内存分析:步骤 7,发布小消息前
log.info("发布小消息前:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 内存影响:
-- - lua内存:增加约176B,消息发送相关结构
-- - sys内存:增加约380B,消息发送缓冲区
local publish_result = mqtt_client:publish("sensor/data", small_msg, 0)
-- 对应内存分析:步骤 8,发布小消息后
log.info("发布小消息后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 7. 发布大消息
-- 内存影响:
-- - lua内存:增加约10328B,存储大消息对象
-- - sys内存:基本不变
local large_msg = string.rep("0123456789", 1000) -- 约 10KB 数据
-- 对应内存分析:步骤 9,发布大消息前
log.info("发布大消息前:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 内存影响:
-- - lua内存:增加约208B,消息发送相关结构
-- - sys内存:增加约12300B,大消息发送缓冲区
local publish_result = mqtt_client:publish("sensor/large_data", large_msg, 0)
-- 对应内存分析:步骤 10,发布大消息后
log.info("发布大消息后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 运行一段时间
sys.wait(5000)
end
-- 8. 断开连接
-- 内存影响:
-- - lua内存:增加约376B,断开连接相关操作
-- - sys内存:减少约12404B,释放连接相关资源
mqtt_client:disconnect()
-- 对应内存分析:步骤 11,断开 MQTT 连接后
log.info("断开 MQTT 连接后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
-- 9. 关闭客户端并释放资源
-- 内存影响:关闭客户端,准备释放资源
mqtt_client:close()
mqtt_client = nil -- 断开引用,等待GC回收
-- 重复执行多次垃圾回收操作,每次间隔1秒
-- 目的是彻底释放MQTT客户端占用的资源,确保内存完全回收
-- 内存影响:
-- - lua内存:减少约49272B,客户端对象和相关结构完全释放
-- - sys内存:减少约33436B,释放所有 MQTT 相关资源
-- 多次执行垃圾回收并间隔等待,确保:
-- 1. Lua的垃圾回收器采用增量式标记-清除算法,多次调用 collectgarbage("collect") 是为了确保更彻底地回收垃圾
-- 2. mqtt是异步操作,使用非阻塞函数和事件回调机制来处理网络操作。某些资源的释放可能依赖于异步操作,需要等待系统完成相关处理
-- 3. 多次回收有助于更有效地整理内存碎片,提高内存使用效率
for i = 1, 5 do
collectgarbage("collect") -- 执行垃圾回收
sys.wait(1000) -- 等待1秒,让系统有时间处理垃圾回收
end
-- 对应内存分析:步骤 12,关闭客户端并多次 GC 后
-- 最终状态:
-- - lua内存:约40248B(接近初始状态的38920B)
-- - sys内存:约108068B(接近初始状态的104760B)
log.info("关闭客户端并 GC 后:")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
log.info("MQTT 客户端资源已释放")
end
-- 创建并启动 MQTT 测试任务
sys.taskInit(mqtt_test_task)




内存使用分析:
步骤 1:初始内存状态
lua 内存:4194296 38920 38920(总大小 4MB,已用 38920B,可用 38920B)
sys 内存:3211584 104760 105880(总大小 3.2MB,已用 104760B,可用 105880B)
步骤 2:网络连接就绪
代码操作:等待网络连接就绪,循环检查 socket.adapter(socket.dft())
内存变化:无明显变化,等待过程不分配额外内存
步骤 3:创建 MQTT 客户端后
代码操作:调用 mqtt.create(nil, SERVER_ADDR, SERVER_PORT) 创建客户端
内存变化:
结论:创建客户端时主要在 lua 内存中分配客户端对象和配置
步骤 4:配置认证信息后
代码操作:调用 mqtt_client:auth(CLIENT_ID, USERNAME, PASSWORD, true) 配置认证
内存变化:
结论:认证配置主要占用 lua 内存,增加较少
步骤 5:连接 MQTT 服务器后
代码操作:调用 mqtt_client:connect() 连接服务器
内存变化:
结论:连接服务器时主要在 sys 内存中分配连接和收发缓冲区
步骤 6:订阅主题后
代码操作:调用 mqtt_client:subscribe("device/commands", 0) 订阅主题
内存变化:
结论:订阅主题主要占用 lua 内存,增加较少
步骤 7:发布小消息前
代码操作:创建约 30 字节的小消息
内存变化:
步骤 8:发布小消息后
代码操作:调用 mqtt_client:publish("sensor/data", small_msg, 0) 发布小消息
内存变化:
步骤 9:发布大消息前
代码操作:创建约 10KB 的大消息(string.rep("0123456789", 1000))
内存变化:
结论:大消息创建时在 lua 内存中分配临时消息对象
步骤 10:发布大消息后
代码操作:调用 mqtt_client:publish("sensor/large_data", large_msg, 0) 发布大消息
内存变化:
结论:大消息发布时在 sys 内存中分配较大的发送缓冲区
步骤 11:断开 MQTT 连接后
代码操作:调用 mqtt_client:disconnect() 断开连接
内存变化:
结论:断开连接时释放部分 sys 内存中的连接资源
步骤 12:关闭客户端并多次 GC 后
代码操作:调用 mqtt_client:close() 关闭客户端,设置 mqtt_client = nil,多次触发 GC
内存变化:
结论:通过多次 GC,lua 内存几乎完全恢复到初始状态(仅增加约 1328B,由网络或者其他任务导致),sys 内存也基本恢复
关键结论
内存分配结构:MQTT 客户端内存使用涉及 lua 和 sys 内存:
内存使用特点:
使用建议:
5.4 Socket 功能内存使用分析
Socket 是网络通信的基础接口,在 LuatOS 中,Socket 的内存使用主要涉及 sys 内存,用于存储发送和接收缓冲区,同时也会使用 lua 内存存储 Socket 对象和配置。LuatOS 的 socket 库是异步非阻塞 API:
-- Socket内存使用情况完整演示
-- 加载libnet库
local libnet = require "libnet"
-- 配置
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 33768
local TASK_NAME = "socket_mem_demo"
-- 内存记录函数
local function log_memory_info(step)
log.info("=== " .. step .. " ===")
log.info("lua:", rtos.meminfo("lua"))
log.info("sys:", rtos.meminfo("sys"))
end
-- 消息回调函数(libnet需要)
local function mem_demo_callback(msg)
log.info("mem_demo回调")
end
-- 主任务函数
local function socket_memory_task()
-- 等待网络就绪
sys.waitUntil("IP_READY", 30000)
-- 记录初始内存
-- 对应内存分析:初始状态,记录系统启动后的内存基线
log_memory_info("初始状态")
-- 1. 创建socket
-- 内存影响:
-- - lua内存:增加约392B,存储socket对象和相关配置信息
-- - sys内存:增加约108B,分配socket控制块和基础结构
local socket_client = socket.create(nil, TASK_NAME)
if not socket_client then
log.error("创建socket失败")
sys.cleanMsg(TASK_NAME)
sys.taskDel(TASK_NAME)
return
end
-- 对应内存分析:创建socket后,检查socket对象分配的内存
log_memory_info("创建socket后")
-- 2. 配置socket
-- 内存影响:配置socket参数,无显著内存变化
if not socket.config(socket_client) then
log.error("配置socket失败")
socket.release(socket_client)
sys.cleanMsg(TASK_NAME)
sys.taskDel(TASK_NAME)
return
end
-- 3. 创建接收缓冲区
-- 内存影响:
-- - lua内存:增加约248B,存储zbuff对象元数据
-- - sys内存:增加约1024B,分配1KB的实际接收缓冲区
local rx_buff = zbuff.create(1024)
-- 对应内存分析:创建缓冲区后,检查1KB缓冲区的内存分配
log_memory_info("创建缓冲区后")
-- 4. 连接服务器
-- 内存影响:
-- - lua内存:增加约1032B,存储连接状态、服务器地址信息
-- - sys内存:减少约284B,内核优化临时缓冲区使用
log.info("正在连接服务器...")
local connect_result = libnet.connect(TASK_NAME, 10000, socket_client, SERVER_ADDR, SERVER_PORT)
if connect_result then
log.info("连接成功")
-- 对应内存分析:连接服务器后,检查连接过程的内存变化
log_memory_info("连接服务器后")
-- 5. 发送数据
-- 内存影响:
-- - lua内存:增加约816B,存储临时发送数据对象和发送状态
-- - sys内存:减少约80B,发送缓冲区数据被网络层处理后释放
local test_data = "内存测试数据 " .. os.time()
local send_result = libnet.tx(TASK_NAME, 5000, socket_client, test_data)
if send_result then
log.info("发送成功:", test_data)
-- 对应内存分析:发送数据后,检查数据发送过程的内存变化
log_memory_info("发送数据后")
else
log.warn("发送失败")
end
-- 6. 等待并接收数据
-- 内存影响:
-- - lua内存:增加约544B,存储接收到的数据对象和接收状态
-- - sys内存:增加约148B,内核处理接收到的数据时的临时缓冲区使用
-- 等待5秒,期间持续检查数据,5秒后无论是否收到数据都继续执行
local wait_start = os.time()
local has_received_data = false
while os.time() - wait_start < 5 do
-- 检查是否有数据
local rx_result = socket.rx(socket_client, rx_buff)
if rx_result then
if rx_buff:used() > 0 and not has_received_data then
log.info("recv data len", rx_buff:used())
-- 对应内存分析:接收数据后,检查数据接收过程的内存变化
log_memory_info("接收数据后")
has_received_data = true
-- 收到数据后不立即跳出循环,继续等待剩余时间
end
else
log.error("socket.rx失败")
-- socket.rx失败也不立即跳出循环,继续等待剩余时间
end
sys.wait(200)
end
log.info("等待5秒接收数据完成")
-- 7. 关闭连接
-- 内存影响:
-- - lua内存:增加约4768B,连接关闭过程中产生的临时对象和状态信息
-- - sys内存:增加约188B,TCP四次挥手过程中的状态管理开销
log.info("正在关闭连接...")
libnet.close(TASK_NAME, 5000, socket_client)
-- 对应内存分析:关闭连接后,检查连接关闭过程的内存变化
log_memory_info("关闭连接后")
else
log.error("连接失败")
end
-- 8. 释放资源
-- 内存影响:
-- - lua内存:增加约168B,资源释放过程中的临时操作
-- - sys内存:减少约1136B,释放socket控制块和1KB接收缓冲区
if socket_client then
socket.release(socket_client) -- 释放socket控制块
socket_client = nil -- 断开引用,等待GC回收
end
if rx_buff then
rx_buff:free() -- 释放zbuff数据块
rx_buff = nil -- 断开引用,等待GC回收
end
-- 对应内存分析:释放资源后,检查资源释放的内存变化
log_memory_info("释放资源后")
-- 9. 强制垃圾回收
-- 重复执行多次垃圾回收操作,每次间隔1秒
-- 目的是彻底释放socket客户端占用的资源,确保内存完全回收
-- 内存影响:
-- - lua内存:减少约9344B,回收所有临时对象、socket对象、缓冲区对象等
-- - sys内存:减少约228B,内核清理剩余的socket相关资源
-- 多次执行垃圾回收并间隔等待,确保:
-- 1. Lua的垃圾回收器采用增量式标记-清除算法,多次调用 collectgarbage("collect") 是为了确保更彻底地回收垃圾
-- 2. socket是异步操作,使用非阻塞函数和事件回调机制来处理网络操作。某些资源的释放可能依赖于异步操作,需要等待系统完成相关处理
-- 3. 多次回收有助于更有效地整理内存碎片,提高内存使用效率
for i = 1, 5 do
collectgarbage("collect") -- 执行垃圾回收
sys.wait(1000) -- 等待1秒,让系统有时间处理垃圾回收
end
-- 对应内存分析:垃圾回收后,检查内存恢复情况
log_memory_info("垃圾回收后")
-- 10. 内存对比总结
-- 最终状态:
-- - lua内存:约42800B(接近初始状态的44176B)
-- - sys内存:约108052B(接近初始状态的108312B)
-- 结论:通过完整的资源释放和多次垃圾回收,内存基本恢复到初始状态
log.info("=== 内存演示完成 ===")
-- 清理task资源
sys.cleanMsg(TASK_NAME)
sys.taskDel(TASK_NAME)
end
-- 使用sys.taskInitEx创建任务(libnet需要)
sys.taskInitEx(socket_memory_task, TASK_NAME, mem_demo_callback)



内存使用分析:
- 初始状态
代码对应:log_memory_info("初始状态")
内存数据:
分析:系统启动后的初始内存状态,此时还未创建任何 socket 相关资源
- 创建 socket 后
代码对应:local socket_client = socket.create(nil, TASK_NAME)
内存变化:
分析:
- 创建缓冲区后
代码对应:local rx_buff = zbuff.create(1024)
内存变化:
分析:
- 连接服务器后
代码对应:local connect_result = libnet.connect(TASK_NAME, 10000, socket_client, SERVER_ADDR, SERVER_PORT)
内存变化:
分析:
- 发送数据后
代码对应:local send_result = libnet.tx(TASK_NAME, 5000, socket_client, test_data)
内存变化:
分析:
- 接收数据后
代码对应:local rx_result = socket.rx(socket_client, rx_buff)
内存变化:
分析:
- 关闭连接后
代码对应:libnet.close(TASK_NAME, 5000, socket_client)
内存变化:
分析:
- 释放资源后
代码对应:
socket.release(socket_client) socket_client = nil rx_buff:free() rx_buff = nil
内存变化:
分析:
- 垃圾回收后
代码对应:
for i = 1, 5 do collectgarbage("collect") sys.wait(1000) end
内存变化:
分析:
- 内存恢复情况
最终状态:
分析:内存基本恢复到初始状态
关键结论
内存分配特点:
使用建议:
不再使用的 MQTT 客户端及时调用 socket.release()释放 socket 资源
使用的 zbuff 缓冲区及时调用 zbuff:free()释放缓冲区资源
lua 层的内存须进行多次垃圾回收才能完全释放
六、内存监控与分析方法
6.1 使用 rtos.meminfo() 进行系统监控
rtos.meminfo() 是 LuatOS 提供的系统级内存信息查询函数,可以获取详细的内存使用统计信息。通过定时调用这个函数并记录返回值,luatools 可以绘制出内存使用曲线,帮助开发者直观地了解内存的变化趋势。
注意
- luatools 3.1.12:提供内存曲线图和 CSV 导出功能
- 定时采样:可设置定时器定期采集内存数据
- 注意需要使用 V2020 版本及以上的版本才行
需要监控内存使用时将下面代码加入自己脚本中间就行
sys.timerLoopStart(function()
log.info("mem.lua", rtos.meminfo())
log.info("mem.sys", rtos.meminfo("sys"))
--log.info("mem.psram", rtos.meminfo("psram")) --需要时打开
end, 3000)


6.2 内存泄漏案例分析
6.2.1 内存泄漏 Demo 脚本
以下是一个简洁的内存泄漏演示:定时器未正确停止,导致对象无法被回收。
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
sys.timerLoopStart(function()
log.info("mem.lua", rtos.meminfo())
log.info("mem.sys", rtos.meminfo("sys"))
log.info("mem.psram", rtos.meminfo("psram"))
end, 3000)
-- 格式化内存输出
local function format_memory()
local total, used, max_used = rtos.meminfo("lua")
return string.format("使用:%.1fKB/%.1fKB(%.1f%%) 峰值:%.1fKB",
used/1024, total/1024, (used/total)*100, max_used/1024)
end
-- 泄漏的数据收集器
local function create_leaky_collector(id)
local collector = {
id = id,
data = {}, -- 存储数据
count = 0
}
-- 定时器持续运行,引用collector导致无法回收
collector.timer_id = sys.timerLoopStart(function()
collector.count = collector.count + 1
collector.data[#collector.data + 1] = {
time = os.time(),
value = math.random(100),
payload = string.rep("X", 100) -- 100字节数据
}
-- 每100次记录一次
if collector.count % 100 == 0 then
log.info("收集器", id, "数据:", collector.count, "内存:", format_memory())
end
end, 100) -- 每100ms执行一次
log.info("创建收集器", id)
return collector
end
-- 主程序:连续创建收集器,但不释放定时器
sys.taskInit(function()
log.info("=== 内存泄漏演示开始 ===")
log.info("初始内存:", format_memory())
local collectors = {}
-- 每2秒创建一个新收集器
for i = 1, 10 do
log.info("\n--- 创建第", i, "个收集器 ---")
local collector = create_leaky_collector("col_" .. i)
collectors[i] = collector
log.info("当前内存:", format_memory())
sys.wait(2000)
end
log.info("\n=== 所有收集器已创建 ===")
log.info("收集器数量:", #collectors)
log.info("当前内存:", format_memory())
-- 尝试释放引用(但定时器还在运行)
log.info("\n尝试释放collectors表...")
collectors = nil
collectgarbage("collect")
log.info("GC后内存:", format_memory())
-- 观察内存变化
for i = 1, 5 do
sys.wait(3000)
log.info("等待", i*3, "秒后内存:", format_memory())
end
log.info("=== 演示结束 ===")
log.info("内存不会下降,因为定时器仍在运行")
end)
运行结果:



6.2.2 泄漏现象与原因
现象:
- Lua 内存持续线性增长,即使对象不再使用
- 内存使用率从初始约 10% 逐渐上升至 90%+
collectgarbage("collect")调用后内存不回落- 最终系统内存耗尽,可能导致崩溃
根本原因:
定时器是 C 层资源,需手动管理生命周期。当定时器闭包引用 Lua 对象,且未调用 sys.timerStop() 时:
- 定时器持续运行 → 闭包始终存活
- 闭包引用
self对象 →self无法被 GC 回收 self对象包含数据缓冲区 → 所有数据无法释放- 循环创建新对象 → 内存持续累积,最终耗尽
6.2.3 修复方案
在 destroy_collector 函中调用 sys.timerStop 正确停止定时器并解除引用:
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
sys.timerLoopStart(function()
log.info("mem.lua", rtos.meminfo())
log.info("mem.sys", rtos.meminfo("sys"))
log.info("mem.psram", rtos.meminfo("psram"))
end, 3000)
-- 格式化内存输出
local function format_memory()
local total, used, max_used = rtos.meminfo("lua")
return string.format("使用:%.1fKB/%.1fKB(%.1f%%) 峰值:%.1fKB",
used/1024, total/1024, (used/total)*100, max_used/1024)
end
-- 泄漏的数据收集器
local function create_leaky_collector(id)
local collector = {
id = id,
data = {}, -- 存储数据
count = 0
}
-- 定时器持续运行,引用collector导致无法回收
collector.timer_id = sys.timerLoopStart(function()
collector.count = collector.count + 1
collector.data[#collector.data + 1] = {
time = os.time(),
value = math.random(100),
payload = string.rep("X", 100) -- 100字节数据
}
-- 每100次记录一次
if collector.count % 100 == 0 then
log.info("收集器", id, "数据:", collector.count, "内存:", format_memory())
end
end, 100) -- 每100ms执行一次
log.info("创建收集器", id)
return collector
end
-- 收集器销毁函数
local function destroy_collector(collector)
if collector and collector.timer_id then
sys.timerStop(collector.timer_id) -- 关键修复:停止定时器
collector.timer_id = nil
collector.data = nil
log.info("已停止收集器", collector.id)
end
return nil
end
-- 主程序:创建收集器,但会正确释放
sys.taskInit(function()
log.info("=== 内存泄漏修复演示开始 ===")
log.info("初始内存:", format_memory())
local collectors = {}
-- 每2秒创建一个新收集器
for i = 1, 10 do
log.info("\n--- 创建第", i, "个收集器 ---")
local collector = create_proper_collector("col_" .. i)
collectors[i] = collector
log.info("当前内存:", format_memory())
sys.wait(2000)
end
log.info("\n=== 所有收集器已创建 ===")
log.info("收集器数量:", #collectors)
log.info("当前内存:", format_memory())
-- 让收集器继续运行一段时间
log.info("\n--- 收集器继续运行10秒 ---")
for i = 1, 5 do
sys.wait(2000)
log.info("运行", i*2, "秒后内存:", format_memory())
end
-- 正确释放所有收集器(关键修复)
log.info("\n正确释放所有收集器...")
for i = 1, #collectors do
collectors[i] = destroy_collector(collectors[i])
end
collectors = nil -- 释放引用
-- 执行GC
collectgarbage("collect")
sys.wait(100) -- 等待GC完成
collectgarbage("collect") -- 再次GC确保清理
log.info("GC后内存:", format_memory())
log.info("=== 演示结束 ===")
log.info("内存已正确回收,无内存泄漏")
end)

修复说明:
- 添加收集器销毁函数(
destroy_collector)
local function destroy_collector(collector)
if collector and collector.timer_id then
sys.timerStop(collector.timer_id) -- 关键修复:停止定时器
collector.timer_id = nil
collector.data = nil
log.info("已停止收集器", collector.id)
end
return nil
end
- 修改主程序的释放逻辑
-- 旧代码:
-- collectors = nil -- 只释放引用,不停止定时器
-- 新代码:
for i = 1, #collectors do
collectors[i] = destroy_collector(collectors[i]) -- 先停止定时器
end
collectors = nil -- 然后释放引用
- 改进 GC 调用
collectgarbage("collect")
sys.wait(100) -- 给GC一点时间
collectgarbage("collect") -- 再次调用确保清理
6.2.4 关键结论
-
常见泄漏场景
-
定时器未停止:最常见的内存泄漏原因
- 事件监听器未移除:回调函数持续持有对象引用
- 全局/静态变量累积:意外将临时对象存入全局表
-
循环引用未打破:对象间相互引用,且无外部访问
-
预防措施
-
配对原则:
创建与释放必须成对出现
local timer = sys.timerStart(...) -- 创建
sys.timerStop(timer) -- 必须释放
- 及时置空:不再使用的对象引用设为
nil - 监控内存:使用
rtos.meminfo("lua")定期检查内存变化
七、常见内存问题及分析
7.1 zbuff 内存操作
zbuff 简介:zbuff 是 LuatOS 中用于直接操作二进制内存数据的库,类似于 C 语言中的内存指针,提供高效的内存管理和操作能力。
核心优势:
-
动态内存管理:
-
自动灵活分配内存
- 支持动态调整缓冲区大小(resize)
- 提供手动释放机制(free),可立即回收内存
-
高效数据操作:
-
原地修改数据,无需创建新对象
- 支持多种数据类型的直接读写(u8/i16/u32/f32 等)
- 提供类似 C 语言的内存操作接口(set/copy/del 等)
-
低内存开销:
-
避免 Lua 字符串不可变性导致的内存碎片
- 适合处理大块二进制数据(如图像、网络包)
- 内存操作效率远高于 Lua 原生类型
-
安全可靠:
-
支持格式化的打包/解包操作
- 自动管理 C 层内存,降低内存泄漏风险
适用场景:
- 处理大量二进制数据(如图像、音频、网络帧)
- 实现高效的协议解析和封装
- 需要频繁修改数据内容的场景
- 对内存占用敏感的应用
7.1 字符串拼接内存爆炸
问题:Lua 字符串是不可变的,每次拼接操作都会创建新的字符串对象,导致内存占用急剧增加。
解决方案:
- 使用
table.concat替代..运算符进行大量字符串拼接。 - 对于二进制数据或需要频繁修改的场景,推荐使用 zbuff 进行内存操作,可避免字符串拼接带来的内存开销
- zbuff 支持原地修改数据,无需创建中间对象,内存效率更高
-- 低效的字符串拼接
local function bad_concat(count)
local s = ""
for i = 1, count do
s = s .. "line " .. i .. "\n"
end
return s
end
-- 高效的字符串拼接
local function good_concat(count)
local parts = {}
for i = 1, count do
parts[#parts + 1] = "line "
parts[#parts + 1] = i
parts[#parts + 1] = "\n"
end
return table.concat(parts)
end
-- 性能对比
local bad_time = os.time()
bad_concat(10000)
bad_time = os.time() - bad_time
local good_time = os.time()
good_concat(10000)
good_time = os.time() - good_time
log.info("String Concat Performance",
"Bad:", bad_time, "ms",
"Good:", good_time, "ms",
"Improvement:", bad_time / good_time, "x")

7.2 大表内存占用过高
问题:向表中插入大量元素时,表的内存占用会显著增加,尤其是在频繁插入和删除操作的场景下。
解决方案:
- 及时清理不再使用的表元素
- 考虑使用分块存储策略
7.3 大文件读取内存不足
问题:一次性读取大文件会占用大量内存,可能导致内存不足错误。
解决方案:采用分块读取或流式处理方式。
-- 分块读取大文件
local function read_file_in_chunks(file_path, chunk_size, callback)
local file = io.open(file_path, "rb")
if not file then
return nil, "Cannot open file"
end
local total_size = 0
while true do
local chunk = file:read(chunk_size)
if not chunk then break end
total_size = total_size + #chunk
if callback then
callback(chunk, total_size)
end
end
file:close()
return total_size
end
-- 使用示例
read_file_in_chunks("/lua/large_file.txt", 8192, function(chunk, total)
-- 处理每个数据块
process_chunk(chunk)
end)
7.4 为什么还有剩余内存却提示内存不足?
问题:collectgarbage("count") 返回的是正在使用的内存,不包括已被释放但尚未被虚拟机重新利用的碎片内存。当需要分配大块连续内存时,即使总可用内存足够,也可能因为碎片化而无法找到满足要求的连续空间。
解决方案:
- 减少内存碎片:避免频繁的小内存分配操作
- 及时执行垃圾回收:在内存操作的关键节点连续调用
collectgarbage("collect")2-3 次,连续调用可确保所有垃圾被彻底回收,减少内存碎片。
八、内存优化建议
8.1 代码编写最佳实践
- 优先使用局部变量:局部变量的生命周期短,更容易被垃圾回收器回收。
-- 错误做法:使用全局变量
global_var = "这是一个全局变量"
-- 正确做法:使用局部变量
local local_var = "这是一个局部变量"
- 及时释放大对象引用:大对象使用完毕后,将其引用设为
nil,并调用collectgarbage("collect")。
-- 创建大对象
local big_table = {}
for i = 1, 10000 do
big_table[i] = string.rep("x", 100)
end
-- 使用完毕后释放
big_table = nil -- 释放引用
collectgarbage("collect") -- 触发GC
- 避免循环引用:两个或多个对象相互引用会形成循环引用,可能导致垃圾回收器无法正确识别并回收这些对象,应尽量避免这种设计。
-- 错误做法:形成循环引用
local obj1 = {}
local obj2 = {}
obj1.ref = obj2
obj2.ref = obj1
-- 正确做法:打破循环引用
obj1.ref = nil
obj2.ref = nil
- 减少全局变量使用:全局变量存储在全局表中,生命周期与程序相同,应尽量减少使用。
-- 错误做法:直接使用全局变量
global_config = { timeout = 1000 }
-- 正确做法:封装为局部变量或模块 local config = { timeout = 1000 }
- 优化表结构:合理设计表结构,避免稀疏数组和混合键类型,提高内存使用效率。
-- 错误做法:混合键类型且数组稀疏
local bad_table = {
name = "test", -- 字符串键
value = 30, -- 字符串键
[1] = 10,
[100] = 20, -- 索引不连续
[200] = 40
}
-- 正确做法1:数组部分和哈希部分分离
local good_table = {
-- 数组部分(连续整数索引,无nil值)
10, 20, 30, 40,
-- 哈希部分(字符串键)
name = "test",
value = 50
}
-- 正确做法2:使用两个专门的表
local array_part = {10, 20, 30, 40} -- 纯数组,高效
local hash_part = {name = "test", value = 50} -- 纯哈希表
实际举例:
-- Lua表结构内存使用测试
local function print_memory_info(description)
local total_lua, used_lua, max_used_lua = rtos.meminfo("lua")
log.info("内存测试", string.format("%s - Lua内存使用量: %d 字节", description, used_lua))
return used_lua
end
local function test_memory_usage()
-- 初始内存状态
local initial_memory = print_memory_info("初始状态")
log.info("内存测试", "\n=== 测试1: 错误做法 - 稀疏数组 + 混合键类型 ===")
local sparse_mixed = {
[1] = "value1",
[1000] = "value2", -- 稀疏数组
[2000] = "value3",
[3000] = "value4",
[4000] = "value5",
[5000] = "value6",
[6000] = "value7",
[7000] = "value8",
[8000] = "value9",
[9000] = "value10", -- 共10个数值元素
name = "test", -- 混合键类型
value = 50
}
local sparse_memory = print_memory_info("创建稀疏混合表后")
log.info("内存测试", string.format("内存增加: %d 字节", sparse_memory - initial_memory))
-- 清理测试1的数据
sparse_mixed = nil
collectgarbage()
initial_memory = print_memory_info("垃圾回收后")
log.info("内存测试", "\n=== 测试2: 正确做法1 - 连续数组 + 混合键类型 ===")
local dense_mixed = {
"value1", "value2", "value3", "value4", "value5",
"value6", "value7", "value8", "value9", "value10", -- 连续数组(10个元素)
name = "test",
value = 50
}
local dense_mixed_memory = print_memory_info("创建密集混合表后")
log.info("内存测试", string.format("内存增加: %d 字节", dense_mixed_memory - initial_memory))
-- 清理测试2的数据
dense_mixed = nil
collectgarbage()
initial_memory = print_memory_info("垃圾回收后")
log.info("内存测试", "\n=== 测试3: 正确做法2 - 分离的数组和哈希表 ===")
local array_part = {
"value1", "value2", "value3", "value4", "value5",
"value6", "value7", "value8", "value9", "value10" -- 纯数组(10个元素)
}
local hash_part = {
name = "test",
value = 50
} -- 纯哈希表
local separate_memory = print_memory_info("创建分离的数组和哈希表后")
log.info("内存测试", string.format("内存增加: %d 字节", separate_memory - initial_memory))
-- 清理测试3的数据
array_part = nil
hash_part = nil
collectgarbage()
print_memory_info("所有测试完成后的最终状态")
end
-- 运行测试
test_memory_usage()
测试结果:

测试结果验证了:
- 稀疏数组确实浪费内存 (测试 1 > 测试 2)
- 连续数组比稀疏数组高效 (测试 2 < 测试 1)
- 单个表的固定开销更优 (测试 2 < 测试 3,数据量小时)
结论:
避免稀疏数组 :索引不连续会导致大量 nil 值占用内存
合理设计表结构 :优先使用连续数组,减少混合键类型的使用
根据数据量选择方案 :小数据量用单表,大数据量考虑分离表
8.2 内存使用检查清单
在实际开发中,遵循以下检查清单可帮助避免常见的内存问题:
- 变量作用域控制:所有变量都使用
local声明,除非确实需要在不同作用域间共享数据 - 及时清理大对象:大对象使用完毕后立即将其引用设为
nil,特别是作为临时缓冲的大表或长字符串 - 循环内优化:避免在循环内部频繁创建临时对象,可将对象创建移到循环外部并复用
- 字符串拼接优化:大量字符串拼接时,使用
table.concat替代..运算符,减少中间字符串的创建 - 大文件处理:处理大文件时采用分块读取或流式处理,避免一次性加载整个文件到内存
- 内存监控:定期检查内存使用峰值和增长趋势,定期通过 collectgarbage("collect") 清除内存
- 垃圾回收调优:根据应用场景调整垃圾回收参数(如
collectgarbage("setpause")和collectgarbage("setstepmul")),平衡性能和内存使用