This article is meant to teach you how to prevent memory leaks and how the Roblox lua garbage collector works.
What is garbage collection?
Garbage collection, for those who are new to the term, is the process that a lot of languages such as JavaScript, python, and lua use to clean up memory. When values are no longer being used, they get garbage collected thus freeing up any memory they used.
What are memory leaks?
Memory leaks may sound like something scary. It might sound like it means information is being exposed somewhere. Memory leaks are far from dangerous though; they are simply sections of memory which are never cleaned up.
Roblox lua vs vanilla lua and lua vs C/C++
Unlike in vanilla lua, it is impossible to force a garbage collection. From what I’ve heard this is to fix a security issue. Garbage collections only take place periodically and these garbage collection periods can occur at any time. The frequency of these collections is not documented and is always subject to change.
In vanilla lua you would call this: collectgarbage("collect")
or you can just call collectgarbage
since collect is the default command. The collect garbage function in Roblox lua however will cause an error if you try either of those. There is still a use for this function though! The "count"
command is usable in Roblox lua. This will return the total memory in use by lua. It is represented in kilobytes. To get the value in bytes you can multiply by 1024.
One important thing to keep in mind is that unlike in C/C++ lua does not allow you to deallocate any memory yourself, this is all done automatically. That means fixing memory leaks is a bit harder to do in lua and memory cannot instantly be cleared up when you want it to.
Weak and Strong References
In lua there are two classes of reference. Most references are strong. Weak references can be used in tables.
A weak reference is a reference that holds some value but doesn’t stop garbage collection. That means if you have a fully weak table it won’t stop any of its values from being garbage collected.
A strong reference is one that will prevent garbage collection. Variables, functions, tables, etc use strong references. Tables can use weak references though.
To give a table weak keys or values you can use the __mode
metamethod. If the value contains k, keys will be weak. If it contains v, values will be weak. Note that I said contains. You can actually use "kv"
or "vk"
to have a fully weak table!
local myOtherTable = {} -- This is used as an example key/value. It doesn't have any special meaning beyond that
local myOtherTable2 = {} -- Same as above
local myTable = {[myOtherTable] = 123, [123] = myOtherTable2, ControlKey = "ControlValue"} -- A strong reference to myTable which is a table that contains some keys and values
myOtherTable = nil -- Make sure we remove the reference to the other table! Our script reference counts as a strong reference.
myOtherTable2 = nil -- Same as above
setmetatable(myTable, {__mode = "k"}) -- Make the table keys weak references
-- The code below will loop through the table. Notice that all keys and values we set when creating the table show up even after __mode has been changed. This is because the garbage collector hasn't run yet.
for key, value in pairs(myTable) do -- Loop through the table
print(key, "=", value) -- Print the contents of the table
end
delay(1.5, function() -- Allow time for the garbage collector to cleanup the script thread
for key, value in pairs(myTable) do -- Loop through the table
print(key, "=", value) -- Print the contents of the table
end
-- Notice that the key referencing the table didn't show up this time! This is because the garbage collector cleaned up our script. Try changing k to v and notice how the result now includes the key but not the value!
-- Also notice how primitives like "ControlValue" weren't garbage collected. The parent thread controls references to these and any scripts which contain a copy of a string will prevent it from being garbage collection. Numbers are not garbage collected at all.
end)
References in functions
When a function references a variable from an outside scope it holds a strong reference to that value! That value can only be garbage collected when the function is garbage collected. This is extremely important information to know in order to prevent memory leaks. For example, when you make a connection to an event the function that is connected can hold a reference to values in your script. If the connection is never disconnected or the instance it is from never gets destroyed the values you reference in the function will exist forever!
local theValue = {"a", "b", "c"}
script.ChildAdded:Connect(function()
print(theValue) -- We reference a variable here! It will never be collected even if the script stops running! To fix this issue theValue must be set to nil (meaning the reference to the table is cleared), the connection must be disconnected, or the script must be destroyed to disconnect the ChildAdded event
end)
Preventing Memory Leaks
Now you know how referencing works in Roblox lua! In order to prevent memory leaks you need to make sure that your scripts don’t hold any unneeded values. The lua do
block is useful for isolating variables so they can be cleaned up quicker. It creates a new scope and once the do block finishes executing any variables created within it are no longer strong references.
local a = {}
do -- A new scope
local b = {}
end
-- The scope has exited! Variable b can be garbage collected!
wait(3) -- The thread (specifically its scope) is still alive so variable a can't be collected yet.
Testing for memory leaks and how to spot them
To test for memory leaks programmatically you can use a table to weakly reference a value:
local ref = setmetatable({myValue}, {__mode="v"})
local function isReferenced()
return ref[1] -- ref[1] can be GCed and if it does this will be nil!
end
There are a few things you should look for to find memory leaks:
-
Functions referencing external variables
Functions which reference external variables hold references to those variables. You should make sure your functions eventually get GCed. Connections prevent a function from being GCed until disconnected! -
Tables referencing values
If you have a table which references a value that table prevents the value from being GCed until the table is GCed. Make sure you clear your tables or set the __mode value to kv if you continue to reference the table somewhere but you are currently done using it. -
Callbacks (e.g. Bindable/RemoteFunction OnInvoke callbacks) referencing values
If a callback references a variable it holds onto it similar to connections. -
Never ending threads
Never ending threads (e.g. using coroutine.yield) never GC their root values. It is important that you enclose temporary values in do blocks in this case or cleanup these values when you’re finished with them.
A note on main script environments
From my testing, root script environments actually never GC (possibly when all child threads complete their execution they do GC) and I was unable to cause a GC even after setting the root and current (level 0/1) environments, setting the environment of all child functions to {}, and wrapping everything in a do block to put everything in a new scope. That includes every global in your script!!
I have no idea the specifics behind how script environments are GCed but it appears that they simply aren’t. If anyone knows anything, please let me know!
A further note on main script environments (update because its sort of needed)
Script environments will currently GC consistently under certain conditions. The following conditions must be met:
- All threads in reference to the environment internally or in your code must be
yielding
ordead
(basically if it isn’t running). - There must be no references to a yielding thread whatsoever as far as I am aware (this doesn’t include all internal referencing, like being tied to the script at all).
That’s all!