查看: 185|回复: 13

[Lua] VVM2服务端Lua内存泄漏:常见原因 + 自制检测工具

[复制链接]

46

主题

42

回帖

1212

积分

金牌会员

积分
1212
发表于 2026-6-5 00:11:21 | 显示全部楼层 |阅读模式
-- MemoryWatcher.lua
-- 定期采样内存,输出增长趋势,帮助定位泄漏点

local M = {}
local _snapshots = {}
local _trackers  = {}  -- 追踪特定表的大小变化

-- 采样一次(依赖引擎提供的内存查询接口)
function M.snapshot(label)
    local kb = collectgarbage("count")
    table.insert(_snapshots, {
        label = label or ("t" .. #_snapshots + 1),
        kb    = kb,
        time  = os.time(),
    })
    return kb
end

-- 打印增长报告
function M.report()
    if #_snapshots < 2 then
        print("[MemWatcher] 至少需要2次采样")
        return
    end
    local base = _snapshots[1]
    print(string.format("%-12s %8s %8s", "标签", "KB", "增长KB"))
    print(string.rep("-", 32))
    for _, s in ipairs(_snapshots) do
        print(string.format("%-12s %8.1f %+8.1f",
            s.label, s.kb, s.kb - base.kb))
    end
end

-- 追踪一个全局表的元素数量变化(找哪个表在无限增长)
function M.track(name, tbl)
    _trackers[name] = tbl
end

function M.checkTrackers()
    for name, tbl in pairs(_trackers) do
        local count = 0
        for _ in pairs(tbl) do count = count + 1 end
        print(string.format("[Track] %-20s 元素数: %d", name, count))
    end
end
配合定时器使用:

local MW = require("MemoryWatcher")

-- 追踪可疑的全局表
MW.track("PlayerCache",   PlayerCache)
MW.track("EventListeners", _listeners)

-- 每5分钟采样一次
local tick = 0
SetTimer(300 * 1000, function()
    tick = tick + 1
    MW.snapshot("t" .. tick .. "(" .. tick*5 .. "min)")
    MW.checkTrackers()

    if tick % 6 == 0 then  -- 每30分钟打印一次报告
        MW.report()
    end
end)

-- ❌ 泄漏1:事件监听器只注册不注销
-- 玩家下线后 listener 仍挂在全局表里
EventBus.on("player_die", function() ... end)  -- 没有对应的 off

-- ❌ 泄漏2:用玩家名做key缓存,玩家下线后不清理
_cache[playerName] = bigObject  -- 永远不删

-- ❌ 泄漏3:闭包意外捕获大对象
local bigTable = loadAllItems()  -- 几千条数据
SetTimer(1000, function()
    -- 只用了 bigTable 的一个字段,但整个 bigTable 被闭包持有
    print(bigTable.count)
end)

-- ✅ 正确做法:只捕获需要的值
local count = loadAllItems().count
SetTimer(1000, function() print(count) end)

return M


2

主题

230

回帖

1908

积分

金牌会员

积分
1908
发表于 2026-6-5 10:11:04 | 显示全部楼层
多发点ai写的东西

25

主题

90

回帖

1437

积分

金牌会员

积分
1437
发表于 2026-6-5 10:25:24 | 显示全部楼层
快了快了,马上就能用VV了

46

主题

42

回帖

1212

积分

金牌会员

积分
1212
楼主 发表于 2026-6-5 13:45:34 | 显示全部楼层

快了快了,马上就能用VV了

25

主题

57

回帖

1417

积分

金牌会员

积分
1417
发表于 2026-6-5 19:23:06 | 显示全部楼层
快了快了,马上就能用VV了

19

主题

14

回帖

666

积分

高级会员

积分
666
发表于 2026-6-17 14:55:10 | 显示全部楼层
-- MemoryWatcher.lua 修复优化版
local M = {}
local _snapshots = {}
local _trackers = {}       -- key:表名, value:{tbl=目标表, lastCount=上次元素数}
local _maxSnapShot = 200   -- 最多保留200条快照,防止无限膨胀
local _enable = true       -- 监控总开关

-- 内部执行完整GC再采样,返回当前内存KB
local function getCleanMemKB()
    collectgarbage("collect") -- 完整垃圾回收
    return collectgarbage("count")
end

-- 采样一次
function M.snapshot(label)
    if not _enable then return 0 end
    local kb = getCleanMemKB()
    local nowSec = os.time()
    local idx = #_snapshots + 1
    local autoLabel = string.format("t%d_%s", idx, os.date("%H:%M:%S", nowSec))
    label = label or autoLabel

    table.insert(_snapshots, {
        label = label,
        kb = kb,
        time = nowSec,
        clock = os.clock()
    })

    -- 超出上限自动清理旧快照
    if #_snapshots > _maxSnapShot then
        table.remove(_snapshots, 1)
    end
    return kb
end

-- 输出完整报告:相对初始基准 + 相邻间隔增量
function M.report()
    if not _enable then return end
    if #_snapshots < 2 then
        print("[MemWatcher] 至少需要2次采样才能生成报告")
        return
    end

    local base = _snapshots[1]
    print("\n===== 内存增长报告 =====")
    print(string.format("%-15s %8s %8s %8s %10s", "标签", "KB", "总增长KB", "间隔增长KB", "间隔秒"))
    print(string.rep("-", 50))

    local prev = base
    for _, s in ipairs(_snapshots) do
        local totalInc = s.kb - base.kb
        local stepInc = s.kb - prev.kb
        local diffSec = s.time - prev.time
        print(string.format(
            "%-15s %8.1f %+8.1f %+8.1f %10d",
            s.label, s.kb, totalInc, stepInc, diffSec
        ))
        prev = s
    end
    print("========================\n")
end

-- 追踪指定表,自动记录历史数量
function M.track(name, tbl)
    if tbl == nil then
        print(string.format("[MemWatcher] 追踪失败 %s:目标表为空", name))
        return
    end
    -- 避免重复覆盖提示
    if _trackers[name] then
        print(string.format("[MemWatcher] 警告:覆盖已有追踪表 %s", name))
    end
    -- 不直接存原始表弱引用,降低常驻风险
    local count = 0
    for _ in pairs(tbl) do count = count + 1 end
    _trackers[name] = {tbl = tbl, lastCount = count}
end

-- 检查所有追踪表,输出数量+变化差值
function M.checkTrackers()
    if not _enable then return end
    print("\n[MemTracker] 表元素监控:")
    for name, info in pairs(_trackers) do
        local tbl = info.tbl
        local curr = 0
        for _ in pairs(tbl) do curr = curr + 1 end
        local diff = curr - info.lastCount
        print(string.format(
            "  %-20s 当前数量:%6d 变化:%+6d",
            name, curr, diff
        ))
        info.lastCount = curr
    end
    print()
end

-- 控制接口:启停、清理数据
function M.setEnable(flag) _enable = flag end
function M.clearAll()
    _snapshots = {}
    _trackers = {}
end

return M

10

主题

85

回帖

954

积分

高级会员

积分
954
发表于 2026-6-17 20:41:59 | 显示全部楼层
看不懂呀

34

主题

74

回帖

1323

积分

金牌会员

积分
1323
发表于 2026-6-19 21:05:49 | 显示全部楼层
快了快了,马上就能用VV了

31

主题

62

回帖

1283

积分

金牌会员

积分
1283
发表于 2026-6-27 02:07:13 | 显示全部楼层
加油啊加啊

31

主题

62

回帖

1283

积分

金牌会员

积分
1283
发表于 2026-6-27 17:16:36 | 显示全部楼层
学习学习
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表