From what seems to be Luau’s closure caching optimization, top-level functions with upvalues that are eligible for caching can cause memory leaks. This can be a very common issue with instances and can very easily be missed since this behavior is not something that the ordinary person is aware of. This was causing memory leaks in my game and making certain temporary instances created during loading/intermission phases to never be collected and build up over time. These instances persist even if they are destroyed via Destroy
.
Reproduction:
-- The ScreenGui instance will never be collected since the closure it is referenced in is cached.
local weak_table = setmetatable({ Instance.new("ScreenGui") }, {__mode = 'v'})
do
local reference = weak_table[1]
local cached_function = function()
-- Force an upvalue capture.
local upvalue = reference
end
cached_function = nil
end
while task.wait(0.1) and #weak_table > 0 do
warn('Waiting for collection...')
end
warn('Done waiting for collection.') -- Never reached.
However, in this case where we can force the optimization to not happen, the instance is collected:
for i = 1, 1 do
local weak_table = setmetatable({ Instance.new("ScreenGui") }, {__mode = 'v'})
do
local reference = weak_table[1]
local cached_function = function()
-- Force an upvalue capture.
local upvalue = reference
end
cached_function = nil
end
while task.wait(0.1) and #weak_table > 0 do
warn('Waiting for collection...')
end
warn('Done waiting for collection.') -- Reached immediately.
end
As shown in the Unique References tab from the Developer Console (the first path is the weak table, and the second path is cached_function
):