使用 tolua_pushusertype_and_takeownership 时 toluapp 是否会造成内存泄漏?

Does toluapp create memory leak when tolua_pushusertype_and_takeownership is used?

这个问题可能是给 Lua 和 lua 专家的。

我正在使用 tolua++1.0.93 和 lua-5.1.4(CEGUI 0.84 依赖项)。 我一直在跟踪这个令人讨厌的内存泄漏几个小时,我发现 toluapp 在 Lua 注册表中创建 tolua_gc table 并且似乎这个 table无限增长。

当我使用 tolua_pushusertype_and_takeownership 将我的对象推送到 Lua 时,我希望 Lua 的 GC 删除我的对象。它确实这样做了,但是 tolua_pushusertype_and_takeownership 调用 tolua_register_gc 将这个对象元 table 放在对象下面作为这个 "global" tolua_gc table 的键。 当 tolua_gc_event 函数调用收集器函数(调用 delete 运算符)时,它将 nil 值设置为刚刚删除的对象下的 tolua_gc table 作为键。这样应该可以,对吧?

嗯,没有。

也许我理解有误,但这似乎对 tolua_gc table 的大小没有影响。 我还尝试从 Lua 手动调用 tolua.releaseownership(object)。它奏效了。我的意思是,它减少了 Lua (LUA_GCCOUNT) 使用的内存,但由于它断开了收集器与对象的连接,因此永远不会调用运算符 delete 并在 C++ 中造成内存泄漏。

这真的很奇怪,因为所有 tolua.releaseownership 所做的就是将传递的对象下的 nil 值设置为 tolua_gc table 作为键。 那么,为什么 tolua.releaseownership 减少了 Lua 使用的内存大小,而 tolua_gc_event 却没有? 唯一的区别是tolua.releaseownership在设置nil为tolua_gc之前调用垃圾收集器table,而tolua_gc_event被垃圾收集器调用(相反的情况)。

为什么我们需要全局 tolua_gc table?我们不能在收集时直接从对象中获取 metatable 吗?

我可以从这个进程中使用的内存非常有限 (8MB),一段时间后这个 tolua_gc table 似乎占据了它的 90%。

我该如何解决这个问题?

谢谢。

编辑: 这些是代码示例:

extern unsigned int TestSSCount;

class TestSS
{
public:
    double d_double;

    TestSS()
    {
//        TestSSCount++;
//        fprintf(stderr, "c(%d)\n",TestSSCount);
    }

    TestSS(const TestSS& other)
    {
        d_double = other.d_double * 0.5;
//        TestSSCount++;
//        fprintf(stderr, "cc(%d)\n",TestSSCount);

    }

    ~TestSS()
    {
//        TestSSCount--;
//        fprintf(stderr, "d(%d)\n", TestSSCount);
    }
};


class App
{
  ...
  TestSS doNothing()
  {
    TestSS t;
    t.d_double = 13.89;
    return t;
  }

  void callGC()
  {
      int kbs_before = lua_gc(d_state, LUA_GCCOUNT, 0);
      lua_gc(d_state, LUA_GCCOLLECT, 0);
      int kbs_after = lua_gc(d_state, LUA_GCCOUNT, 0);
      printf("GC changed memory usage from %d kB to %d kB, difference %d kB",
              kbs_before, kbs_after, kbs_before - kbs_after);
  }

  ...
};

这是 .pkg 文件:

class TestSS
{
public:
    double d_double;
};

class App
{
    TestSS doNothing();
    void callGC();
};

现在完成 Lua 代码(app 和 rootWindow 是作为常量提供给 Lua 的 C++ 对象):

function handleCharacterKey(e_)
    local key = CEGUI.toKeyEventArgs(e_).scancode

    if key == CEGUI.Key.One then
  for i = 1,10000,1 do
    -- this makes GC clear all memory from Lua heap but does not call destructor in C++
    -- tolua.releaseownership(app:doNothing())
    -- this makes GC call destructors in C++ but somehow makes Lua heap increase constantly
    app:doNothing()
    elseif key == CEGUI.Key.Zero then
        app:callGC()
    end
end

rootWindow:subscribeEvent("KeyUp", "handleCharacterKey")

这是我按下 0 1 0 1 0 1 0 时得到的输出:

这是我用tolua.releaseowenership

的时候
GC changed memory usage from 294 kB to 228 kB, difference 66 k
GC changed memory usage from 228 kB to 228 kB, difference 0 kB
GC changed memory usage from 228 kB to 228 kB, difference 0 kB
GC changed memory usage from 228 kB to 228 kB, difference 0 kB

这个没有tolua.releaseownership:

GC changed memory usage from 294 kB to 228 kB, difference 66 kB
GC changed memory usage from 605 kB to 604 kB, difference 1 kB
GC changed memory usage from 982 kB to 861 kB, difference 121 kB
GC changed memory usage from 1142 kB to 1141 kB, difference 1 kB

这没有释放所有权,但我在键盘上按下的序列是 0 1 0 1 0 1 0 0 0 0(最后再额外调用 GC 三个)

GC changed memory usage from 294 kB to 228 kB, difference 66 kB
GC changed memory usage from 603 kB to 602 kB, difference 1 kB
GC changed memory usage from 982 kB to 871 kB, difference 111 kB
GC changed memory usage from 1142 kB to 1141 kB, difference 1 kB
GC changed memory usage from 1141 kB to 868 kB, difference 273 kB <- this is after first additional GC call
GC changed memory usage from 868 kB to 868 kB, difference 0 kB
GC changed memory usage from 868 kB to 868 kB, difference 0 kB

问题不是错误或内存泄漏。尽管如果您的内存确实有限,您可以说这是内存泄漏。问题是 tolua_gc 是 lua table,当你通过将它们设置为 nil 来删除元素时不会重新散列。

虽然我认为这可能是问题所在,但我太愚蠢了,没有看到它是否属实。所以垃圾收集器不能强制 table 重新散列和缩小它的大小。所以 table 会增长,直到某些插入会触发重新散列。 读这个:http://www.lua.org/gems/sample.pdf

所以最后我删除了 tolua_gc table 并放入了 metatables(tolua 曾经放在 tolua_gc table 以 lightuserdata 作为键)作为 userdata 对象本身的特殊字段。 而且我现在不是从 tolua_gc table 访问那些元 table,而是从对象本身获取它。其他都是一样的,好像也行。