Preface
I’ve seen several experienced developers not knowing this, or not having a full understanding of what’s going on, so I thought I’d write a dedicated post on the issue of accidentally leaking Instances (so that they are not GCed even when there are no references to them) through lingering event connections.
What circumstances cause a leak
do -- All good, this part will be GCed just fine
local p = Instance.new('Part')
p.Touched:connect(function() print("Touched") end)
end
do -- OOPS!... this part will hang around in memory forever!
local p = Instance.new('Part')
p.Touched:connect(function() print(p) end)
end
do -- OOPS!... still leaking, it doesn't have to be a direct reference!
local p = Instance.new('Part')
local dataTable = {Message = "Test"; Part = p}
p.Touched:connect(function() print(dataTable.message) end)
end
do -- All good, we break the connection so the part is free to be GCed
local p = Instance.new('Part')
local cn = p.Touched:connect(function() print(p) end)
cn:disconnect()
end
do -- Also also all good, as Destroy() implicitly disconnects all connections
local p = Instance.new('Part')
p.Touched:connect(function() print(p) end)
p:Destroy()
end
Why the leak happens
The core of this problem comes down to the fact that the “connection list” of connected functions that Roblox objects store is a list opaque references, which just tell Lua “there is a reference to this function”, without telling Lua anything about where that reference is coming from. That means that you can get a circular reference which the garbage collector can’t recognize and collect. From the second example above where the part gets leaked indirectly:
-
The closure “
function() print(dataTable.Message) end
” has a reference to the dataTable, through the upvalue “dataTable”. -
The dataTable has a reference the part as one of it’s members
-
The part has a connection list, which includes a reference to the closure
-
Normally you might expect this cycle to not be an issue, since the Lua garbage collector can detect cycles with no other references
-
However the connection list is a C++ list of “I have a reference to this Lua function” references, so Lua has no way of knowing that the only reference to that closure in fact comes from the very part it has a reference to (indirectly through the table)
-
RIP client/server RAM if you ran this code really frequently
How to prevent it
The solution is very simple. Always :Destroy()
Instances that you’re not going to use again… unless of course they’re things that you’re not holding any script references to, then they’ll GC nicely regardless of what you do. Destroying breaks any connections that an object has, and does it recursively, so if you just destroy all of your top level objects that will clear out any of these problematic connections that would otherwise leak references.
Easy… unless, you’re like me and you use an object oriented style with a custom “Signal” class to make events on your Lua objects. If you use a BindableEvent object internally to implement that Signal class, then you’d better make damn sure that you’re actually disconnecting all of the connections that you make on any copies of those Signal objects with a lifetime shorter than that of the server/client that they’re on. @Quenty has a very nice pattern for this with his “Maid” class that you can find in multiple of his projects if you need a way to fix it. My approach is to literally put a “Destroy” method on my Lua objects which disconnects all the connections / Destroys the sub-Lua-objects and call it when I’m done with something, since I find the Maid pattern a bit obfuscating.
EDIT 8/2/2021: With the arrival of the task
library it’s now possible to write a correct performant pure Lua (that is, does not use any Instances internally) Signal implementation which will never leak memory to avoid the issues described in the above paragraph. You can find my pure Lua “GoodSignal
” implementation here if you need one.
Edit: A note on temporary scripts
If I recall correctly (and as brought up by some other posters), there is some behavior to try to break connections made in temporary scripts (for instance, scripts under the character) when the scripts are removed. I don’t know the full details on that since I write almost all of my scripts as permanent ones to avoid race conditions, I’ve never explored the right way to write temporary variants.
No matter how you look at it though, this issue is far more important on the server, because most client visits are short enough that even if there are some small leaks there won’t be time for them to add up to a problematic level.
Closing
If you didn’t know about this, you should probably take a second look at your game code to make sure you haven’t fallen into this trap anywhere, especially if you’re using a custom Signal class, since it’s so easy to be lazy and not store the connection objects for those.