LuatOS 开发笔记:Lua GC 原理与调优方法

嵌入式领域开发有一个共识:内存资源稀缺且宝贵。系统内核、业务逻辑、UI 交互等所有模块,都必须兼顾内存占用问题。

本系列前文已讲解 RAM 基础、Lua 内存模型与 LuatOS 内存分配方式,本篇将重点解析 Lua 虚拟机垃圾回收机制的工作原理。GC 可自动甄别并释放程序闲置内存,适配嵌入式资源受限的开发场景,降低内存管理的开发难度。

一、垃圾回收工作原理

Lua采用增量标记-扫描(Incremental Mark-and-Sweep) 算法进行GC垃圾回收,这是一种高效的自动内存管理机制。

该算法的工作过程分为两个主要阶段:

  • 标记阶段(Mark Phase)

    从根对象(全局变量、活跃的局部变量、寄存器等)开始,遍历所有可达对象,将其标记为存活状态。

  • 扫描阶段(Sweep Phase)遍历所有对象,回收未被标记的对象,将其占用的内存标记为可用。

实际应用中,Lua的增量垃圾回收将标记和扫描过程分解为多个小步骤,与程序执行交替进行。这种设计可以减少垃圾回收对程序响应时间的影响,特别适合实时性要求较高的嵌入式系统。

二、垃圾回收示例

下面通过一个具体的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")
运行结果.png

运行结果分析:

  • 创建对象后,内存显著增加。

  • 函数返回后,unreachable及其链表失去引用,成为“垃圾”但尚未回收。

  • 手动GC后,内存下降,不可达对象被清理。

**关键点说明:**循环引用不影响GC——cycle1和cycle2互相引用,但因为它们仍被外部active_cycle引用,所以是可达的,不会被回收。只有当整个循环引用链失去外部引用时,才会被正确回收。

三、控制GC行为的两个关键参数

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

3.1 间歇率(Pause)

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

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

  • 值越大,垃圾回收间隔越长,内存占用越高,但CPU开销越小。

3.2 步进倍率(Step Multiplier)

控制垃圾回收器的工作速度相对于内存分配速度的倍率。默认值为200,表示垃圾回收器以内存分配速度的两倍工作。

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

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

四、collectgarbage() 核心用法

Lua提供了collectgarbage([opt[, arg]]) 函数用于控制自动内存管理,该函数在LuatOS中同样可用。

4.1 函数主要用法

01 函数核心用法26041601NEW.png

4.2 重点参数使用示例

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

下面通过一个详细的示例,结合日志输出重点讲解:

-- 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

1)“count” 参数

功能说明: 返回Lua虚拟机当前使用的总内存,单位为KB(带小数精度)。

使用场景:

  • 监控内存使用趋势;

  • 调试内存泄漏问题;

  • 评估垃圾回收效果。

日志解析:

=== 1. 初始内存状态 ===

初始内存: 120.50 KB

=== 2. 创建大量对象 ===

创建 5000 个对象后内存: 356.75 KB

内存增加: 236.25 KB

关键说明:

返回值包含小数部分,提供精确的内存使用情况;包括Lua VM管理的所有内存,包括已分配但未使用的内存;是监控内存变化的主要工具。

2)“collect” 参数

功能说明: 执行一次完整的垃圾回收循环,包括标记、扫描和清理阶段。

使用场景:

  • 手动触发垃圾回收,释放不再使用的内存;

  • 在内存敏感操作前清理内存;

  • 测试和调试垃圾回收行为。

日志解析:

=== 4. 使用 collect 参数触发垃圾回收 ===手动 GC 前内存: 356.75 KB手动 GC 后内存: 220.30 KBGC 释放内存: 136.45 KB

关键说明:

执行完整的垃圾回收流程,释放所有不可达对象;阻塞执行,直到垃圾回收完成;适用于需要立即释放内存的场景。

3)“setpause” 参数

功能说明: 设置垃圾回收器的间歇率,控制垃圾回收开始的阈值。

工作原理:

  • 间歇率默认值为200,表示当总内存使用量达到上次GC后活跃内存的2倍时,触发新一轮GC;

  • 公式:触发条件=当前内存>(基准内存×pause/100);

  • 基准内存:每次GC完成后更新的活跃内存量;

  • pause值:百分比单位,100 表示 100%。

使用场景:

  • 调整垃圾回收频率,平衡内存使用和CPU开销;

  • 针对不同应用场景优化垃圾回收行为;

  • 在内存紧张时降低间歇率,提高回收频率。

关键说明:

  • 间歇率越低,垃圾回收越频繁,内存占用越低,但CPU开销越大;

  • 间歇率越高,垃圾回收间隔越长,内存占用越高,但CPU开销越小;

  • 不同应用场景需要不同的设置:

    内存敏感应用:设置较低的间歇率(如100-150);

    CPU敏感应用:设置较高的间歇率(如300-500);

    平衡需求:使用默认值200。

五、最佳实践应用

在实际开发中,合理运用GC相关参数和监控手段,能有效提升程序的内存稳定性和运行效率。

5.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)

5.2 手动GC时机

在内存敏感操作前(如加载大图片、接收大文件)手动执行GC,确保内存充足。

-- 在内存敏感操作前执行手动 GC
collectgarbage("collect")  -- 清理内存

5.3 间歇率优化

根据应用场景调整间歇率,平衡内存与性能。

-- 根据应用场景调整间歇率
collectgarbage("setpause", 150)  -- 更频繁的 GC
--collectgarbage("setpause", 300)  -- 较少的 GC,节省 CPU

理解增量标记-扫描的原理,掌握collectgarbage的用法,你就能在内存占用和CPU开销之间找到最佳平衡点。配合前两期的内存分区知识,你已经具备了LuatOS内存管理能力。

针对嵌入式内存紧张问题,梳理 Lua 虚拟机 GC 核心知识。包含增量标记 - 扫描工作流程、循环引用回收规则、间歇率与步进倍率调节逻辑、collectgarbage 函数常用参数,以及内存监控、手动 GC、参数适配等实际开发落地要点。

今天的内容就先分享到这里!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容