跳转至

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 模组来说:

  1. 物理内存构成:780EHM 使用的是 EC718HM 系列芯片,该系列芯片配备了物理 PSRAM
  2. 内存区域映射

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 内存信息。不同芯片平台、固件编译配置下的内存分配和使用方式存在显著差异。

内存区域与物理内存关系

首先需要理解物理内存和逻辑内存的区分:

  1. 物理内存类型(用大写的 SRAM/PSRAM 代表物理内存):

  2. SRAM:静态随机存取存储器,访问速度快但容量较小;一定存在, 但不一定会暴露给客户使用

  3. PSRAM:外接伪静态随机存取存储器,访问速度较慢但容量较大;不一定存在, 也不一定会启用给客户使用
  4. 注意 区分 SRAM 和 PSRAM 的差异, 单从硬件上说, SRAM 更快但小, PSRAM 慢但更大.
  5. 逻辑内存类型(用户可见), sys/psram/lua 是 luatos 暴露给用户展示的内存分配布局:

  6. 虚拟机内存("lua"):Lua 虚拟机使用的内存区域,包括 Lua 脚本中的变量、函数、表、字符串等

  7. 系统内存("sys"):系统级内存区域,用于 FreeRTOS 任务栈、驱动缓冲区等
  8. 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
仅用于开发调试

各区域主要功能

  1. 虚拟机内存("lua"):

  2. Lua 脚本执行环境

  3. Lua 变量、函数、表、字符串等动态分配
  4. 由 Lua 垃圾回收器管理
  5. 开发关注重点:避免内存泄漏,控制内存使用峰值
  6. 系统内存("sys"):

  7. FreeRTOS 内核堆(Heap)

  8. 任务栈、中断栈
  9. 驱动缓冲区(GPIO、UART、I2C 等)
  10. 系统级任务:任务调度、中断处理、消息队列等
  11. 用户涉及有限:主要通过 zbuff 接口间接使用
  12. PSRAM 内存("psram"):

  13. 大容量数据缓存(图像/音频处理)

  14. 网络通信大文件接收(TCP/UDP/OTA)
  15. zbuff 缓冲区分配
  16. 注意:并非所有平台都支持 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) 算法进行垃圾回收,这是一种高效的自动内存管理机制。该算法的工作过程分为两个主要阶段:

  1. 标记阶段(Mark Phase):从根对象(全局变量、活跃的局部变量、寄存器等)开始,遍历所有可达对象,将其标记为"存活"状态。
  2. 扫描阶段(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. 步骤 1:标记根对象
_G.global_table (标记为存活)
active_table (标记为存活)
active_cycle (标记为存活)
unreachable (未标记)
  1. 步骤 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. 垃圾回收步骤 1:标记一部分根对象
  3. 程序继续执行
  4. 垃圾回收步骤 2:遍历标记一部分可达对象
  5. 程序继续执行
  6. 垃圾回收步骤 3:继续标记剩余可达对象
  7. 程序继续执行
  8. 垃圾回收步骤 4:扫描一部分内存,回收未标记对象
  9. 程序继续执行
  10. 垃圾回收步骤 5:完成剩余内存的扫描和回收

这种交替执行的方式避免了长时间暂停程序执行,适合实时性要求较高的嵌入式系统。

4.2 垃圾回收控制参数

Lua 垃圾回收器的行为由两个关键参数控制:

  1. 间歇率(Pause):控制垃圾回收器开始新一轮回收的阈值。默认值为 200,表示当内存使用量达到上次回收时的两倍时,开始新的回收循环。

  2. 值越小,垃圾回收越频繁,内存占用越低,但 CPU 开销越大

  3. 值越大,垃圾回收间隔越长,内存占用越高,但 CPU 开销越小
  4. 步进倍率(Step Multiplier):控制垃圾回收器的工作速度相对于内存分配速度的倍率。默认值为 200,表示垃圾回收器以内存分配速度的两倍工作。

  5. 值小于 100 时,垃圾回收速度慢于内存分配速度,可能导致内存无限增长

  6. 值越大,垃圾回收越积极,每次回收的内存越多

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 最佳实践

  1. 内存监控
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)
  1. 手动 GC 时机
-- 在内存敏感操作前执行手动 GC
collectgarbage("collect")  -- 清理内存
  1. 间歇率优化
-- 根据应用场景调整间歇率
collectgarbage("setpause", 150)  -- 更频繁的 GC
--collectgarbage("setpause", 300)  -- 较少的 GC,节省 CPU

通过合理使用这三个核心参数,可以有效监控和控制 Lua 虚拟机的内存使用,优化应用程序的性能和稳定性。

五、实际功能内存使用案例分析

为了更好地理解 LuatOS 中三种内存的使用情况,下面结合具体的脚本代码,用 780EHM 来详细分析不同功能在运行过程中如何使用 luasyspsram 内存。

对于 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 层数据块(大,存储实际数据),实现了高效的内存管理:

  1. 大内存块在 sys 分区/psram 分区 中分配,不占用 Lua 内存
  2. 支持手动释放数据块,避免内存泄漏
  3. 创建大内存时自动触发 GC,优化内存使用
  4. 适合处理大块二进制数据,如网络数据、文件数据等

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 客户端内存使用涉及 luasys 内存:

内存使用特点:

使用建议:

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)

内存使用分析:

  1. 初始状态

代码对应:log_memory_info("初始状态")

内存数据:

分析:系统启动后的初始内存状态,此时还未创建任何 socket 相关资源

  1. 创建 socket 后

代码对应:local socket_client = socket.create(nil, TASK_NAME)

内存变化:

分析:

  1. 创建缓冲区后

代码对应:local rx_buff = zbuff.create(1024)

内存变化:

分析:

  1. 连接服务器后

代码对应:local connect_result = libnet.connect(TASK_NAME, 10000, socket_client, SERVER_ADDR, SERVER_PORT)

内存变化:

分析:

  1. 发送数据后

代码对应:local send_result = libnet.tx(TASK_NAME, 5000, socket_client, test_data)

内存变化:

分析:

  1. 接收数据后

代码对应:local rx_result = socket.rx(socket_client, rx_buff)

内存变化:

分析:

  1. 关闭连接后

代码对应:libnet.close(TASK_NAME, 5000, socket_client)

内存变化:

分析:

  1. 释放资源后

代码对应:

socket.release(socket_client) socket_client = nil rx_buff:free() rx_buff = nil

内存变化:

分析:

  1. 垃圾回收后

代码对应:

for i = 1, 5 do collectgarbage("collect") sys.wait(1000) end

内存变化:

分析:

  1. 内存恢复情况

最终状态:

分析:内存基本恢复到初始状态

关键结论

内存分配特点:

使用建议:

不再使用的 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() 时:

  1. 定时器持续运行 → 闭包始终存活
  2. 闭包引用 self 对象 → self 无法被 GC 回收
  3. self 对象包含数据缓冲区 → 所有数据无法释放
  4. 循环创建新对象 → 内存持续累积,最终耗尽

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)

修复说明

  1. 添加收集器销毁函数(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
  1. 修改主程序的释放逻辑
-- 旧代码:
-- collectors = nil  -- 只释放引用,不停止定时器

-- 新代码:
for i = 1, #collectors do
    collectors[i] = destroy_collector(collectors[i])  -- 先停止定时器
end
collectors = nil  -- 然后释放引用
  1. 改进 GC 调用
collectgarbage("collect")
sys.wait(100)  -- 给GC一点时间
collectgarbage("collect")  -- 再次调用确保清理

6.2.4 关键结论

  1. 常见泄漏场景

  2. 定时器未停止:最常见的内存泄漏原因

  3. 事件监听器未移除:回调函数持续持有对象引用
  4. 全局/静态变量累积:意外将临时对象存入全局表
  5. 循环引用未打破:对象间相互引用,且无外部访问

  6. 预防措施

  7. 配对原则:创建释放 必须成对出现

local timer = sys.timerStart(...)  -- 创建
sys.timerStop(timer)               -- 必须释放
  • 及时置空:不再使用的对象引用设为 nil
  • 监控内存:使用 rtos.meminfo("lua") 定期检查内存变化

七、常见内存问题及分析

7.1 zbuff 内存操作

zbuff 简介:zbuff 是 LuatOS 中用于直接操作二进制内存数据的库,类似于 C 语言中的内存指针,提供高效的内存管理和操作能力。

核心优势

  1. 动态内存管理

  2. 自动灵活分配内存

  3. 支持动态调整缓冲区大小(resize)
  4. 提供手动释放机制(free),可立即回收内存
  5. 高效数据操作

  6. 原地修改数据,无需创建新对象

  7. 支持多种数据类型的直接读写(u8/i16/u32/f32 等)
  8. 提供类似 C 语言的内存操作接口(set/copy/del 等)
  9. 低内存开销

  10. 避免 Lua 字符串不可变性导致的内存碎片

  11. 适合处理大块二进制数据(如图像、网络包)
  12. 内存操作效率远高于 Lua 原生类型
  13. 安全可靠

  14. 支持格式化的打包/解包操作

  15. 自动管理 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 大表内存占用过高

问题:向表中插入大量元素时,表的内存占用会显著增加,尤其是在频繁插入和删除操作的场景下。

解决方案

  1. 及时清理不再使用的表元素
  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") 返回的是正在使用的内存,不包括已被释放但尚未被虚拟机重新利用的碎片内存。当需要分配大块连续内存时,即使总可用内存足够,也可能因为碎片化而无法找到满足要求的连续空间。

解决方案

  1. 减少内存碎片:避免频繁的小内存分配操作
  2. 及时执行垃圾回收:在内存操作的关键节点连续调用 collectgarbage("collect") 2-3 次,连续调用可确保所有垃圾被彻底回收,减少内存碎片。

八、内存优化建议

8.1 代码编写最佳实践

  1. 优先使用局部变量:局部变量的生命周期短,更容易被垃圾回收器回收。
-- 错误做法:使用全局变量
   global_var = "这是一个全局变量"

   -- 正确做法:使用局部变量
   local local_var = "这是一个局部变量"
  1. 及时释放大对象引用:大对象使用完毕后,将其引用设为 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
  1. 避免循环引用:两个或多个对象相互引用会形成循环引用,可能导致垃圾回收器无法正确识别并回收这些对象,应尽量避免这种设计。
-- 错误做法:形成循环引用
   local obj1 = {}
   local obj2 = {}
   obj1.ref = obj2
   obj2.ref = obj1

   -- 正确做法:打破循环引用
   obj1.ref = nil
   obj2.ref = nil
  1. 减少全局变量使用:全局变量存储在全局表中,生命周期与程序相同,应尽量减少使用。
-- 错误做法:直接使用全局变量
global_config = { timeout = 1000 }

-- 正确做法:封装为局部变量或模块 local config = { timeout = 1000 }
  1. 优化表结构:合理设计表结构,避免稀疏数组和混合键类型,提高内存使用效率。
-- 错误做法:混合键类型且数组稀疏
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")),平衡性能和内存使用