Garbage Collection and Memory Leaks in Roblox - What you should know

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:

  1. 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!
  2. 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.
  3. Callbacks (e.g. Bindable/RemoteFunction OnInvoke callbacks) referencing values
    If a callback references a variable it holds onto it similar to connections.
  4. 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:

  1. All threads in reference to the environment internally or in your code must be yielding or dead (basically if it isn’t running).
  2. 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!

153 Likes
We need more information on the garbage collector
:Destroy() and culling references, seperate things!
Un-Referenced Object/Table Garbage Collection
Questions about garbage collection
Can weak values in a table avoid memory leaks from lost table refs?
Will this cause a memory leak?
Script Removal and Garbage Collection
CollectionService, .Touched events and tracking Hit parts
This script seems to be slowing my game down a lot
Memory reduction
Scripting Resources Mega List
Roblox Scripting Roadmap & Learning Resource List
Game getting slow
How to get a Random Bool Value that False
Experimenting with Untracked Memory and tables
Make only 1 image visible when last checked image was visible (hard to explain)
Does this function that gets used only once get garbage collected?
Does this create a memory leak?
Properly using RunService along with preventing Memory Leaks
Rate limiter module for Remotes
What is garbage collecting and when should I manually do it and when is it automatic?
Resources on Scripting help | Resource Compilation
Would this be a cause for memory leaks?
How do I detect memory leaks?
Performance considerations, improvements, and optimizations when making a game
CollectionService Memory Leaks?
Is clearing an array explicitly by setting values to nil by setting values to nil better letting the GC handling it?
Table can't set itself to nil i.e. can't destroy itself
How do I spot and avoid memory leak?
About Roblox Memory Leaks & Questions
Will localizing a tween function create a memory leak?
Recycler | For your memory recycling needs
[Closed] Experienced Programmer & Code Reviewer
Engine Bug: Core Memory/default engine leak
How efficient is this loading screen?
Guis in StarterGui don't GC?
Help with ore respawning script
Game lags after 5 mins of playing
Using :Destroy() on a Instance doesn't properly cleanup memory
Does this create a memory leak?
Help with remote event
Is Adornee a strong reference?
How do I make my game less laggy?
About connections and deleting it

Numbers are never garbage collected, they’re not GCObjects. Strings from literals should not GC either until the owning function (not the function instance) is collected or no copies of the string exist anywhere.

This is useful advice but if your script only runs once and just connects a bunch of stuff and such once it finishes executing it’ll have the same effect right away.

The string literal would probably never be collected in this case.

18 Likes

Although not totally necessary, I think it would be useful for this post to include some information about low-level memory concepts. Although the Lua VM handles all of this for you, at the end of the day, you are still controlling this memory.

I would suggest talking about stack, heap and how local, global and dynamic allocations work in that context. Without that kind of knowledge, I feel as though the term “memory leak” and similar can be confusing and possibly misleading. It is important for people coming to Lua from a C/C++ background (like myself) to understand that they have lost some of the control they are used to, and can’t simply find some places they forgot to de-allocate to reduce memory use.

Also, I think an example of a memory leak and how to spot one would be very helpful for anybody stumbling upon this looking to combat high memory usage in a game. Maybe show a working example and how you would go about fixing it.

Finally, a more in-depth explanation of strong and weak references would be good. At the moment, somebody coming on the thread to find out about that would be at a loss. What do the terms mean? How do they affect GC? People coming for a beginner explanation would like to find out.

10 Likes

Thank you for the correction! I’ll fix this in my post.

@CodeNinja16
To be completely honest I do not know much about how lua garbage collection works internally. I will add information about memory leaks related to C/C++ as well since I do agree that it is useful.

I also have an easy method for testing for memory leaks programmatically using tables to weakly reference values and test if they have been garbage collected. This allows for many cool things beyond that such as watching for garbage collections.

I also agree that my explanation is a little too short I had to squeeze it in since I was running out of time to write and I wanted to at least get the basics in.

3 Likes

I’ve heard a lot that never Disconnected connections can lead to memory leaks, especially if you have many.
Is this because the connections are only GCed when Disconnected?

2 Likes

Yep! Think about it like this: in order for a connected function to be called it has to remain in memory. If values it references get GCed it will break when trying to use those values. But when you Disconnect the connection the callback is no longer needed.

So you can think of a connection as keeping a variable referencing your callback function and calling it when it is fired.

Or more specifically, a table with strong keys and values with all connection callbacks.

1 Like

Side note: You don’t have to disconnect connections when you expect that the object that the events are attached to will be destroyed, because then the connections are automatically cleaned up.

7 Likes

At some point, I think I’ll write an article about how memory works in computers at a low level, saying as it is important to understand leaks and some performance issues. Many new and old developers get used to high level memory usage (like no strict type system and dynamic typing) and never get to learn how the memory they control actually works.

Thank you, that’s just what I was looking for

1 Like

Memory at the low level is way out of scope and over complicated for just Lua development. I suggest instead to document how the Lua GC works, if Roblox hasn’t changed it much.

4 Likes

I disagree entirely, but I see your point. In my opinion, if you don’t understand even the basics of how memory works at a low-level, it becomes difficult for you to optimise it. Regardless of how you program in Lua, your program will be using the same concepts at a low-level.

If you think about Lua as a tool that does the low-level memory handling for you, you should still learn how the tool works so you can make the way you use it as optimally as possible.

4 Likes

I understand how memory works at a low level (in fact I botched together some C# code to let me execute CPU instructions by overwriting a delegate’s memory)

I don’t think that this information helps with understanding Roblox GC all that much though. It is good to know how stuff works but in this case I don’t think it helps all that much to explain it. I do think that in the context of memory leaks it might make sense to include a simple explanation of it though.

2 Likes

This is helpful. I usually put my memory management down to common sense (e.g. disconnecting connections that are no longer needed) but it’s nice to unlift the veil a little bit.

I would love to learn more about how low level memory works, especially in Lua, but it’s intimidating from someone without a CS background–and unclear what the benefits will be.

1 Like

CSharp is a language which has a garbage collector built in as well so you’d only need to worry about memory stuff in C/C++ code. But you can think of memory like a really long list of bytes (like a file). Some sections in memory can be used to store values (and in windows it can be marked with different flags such as executable, readable, writable, etc). When you make a value it gets stored in memory somewhere. When a value gets garbage collected that section of memory is cleaned up and freed so other programs can use it.

So in a really basic way memory is one big shared file between a bunch of programs but programs only have access to some specific regions of that big file. And information is stored there for later use by the program.

2 Likes

You mention coroutines. Does that mean:

coroutine.wrap(function()
 --Do Stuff
 coroutine.yield()
end)()

Is a memory leak?
Or not using coroutine.yield() is a memory leak?

I think that if the coroutine function isn’t a never ending loop by itself and just does a one-shot function there’s no need to do coroutine.yield(), however I may be wrong and would like to be corrected if needed.

That was originally what I believed to be the case, however, the last time I did tests I discovered that that is actually false.

A permanently yielding thread can (and will) GC if it has no live references to it. This is convenient if you want to halt a thread from inside of a pcall, you can coroutine.yield() and as long as all references to the thread die it will GC on its own. (As of currently at least)

6 Likes

Whenever a function ends (returns), a CLOSE instruction is performed where any up values that the function uses are closed and moved to the up value list of that function when they have a reference or freed from the memory when they don’t.

This is C function is responsible for that:

void luaF_close (lua_State *L, StkId level) {
  UpVal *uv;
  while (L->openupval != NULL && (uv = L->openupval)->v >= level) {
    lua_assert(upisopen(uv));
    L->openupval = uv->u.open.next;  /* remove from 'open' list */
    if (uv->refcount == 0)  /* no references? */
      luaM_free(L, uv);  /* free upvalue */
    else {
      setobj(L, &uv->u.value, uv->v);  /* move value to upvalue slot */
      uv->v = &uv->u.value;  /* now current value lives here */
      luaC_upvalbarrier(L, uv);
    }
  }
}

Each function has it’s own up value list, however I’m a bit intrigued on when that up value list is cleared / emptied? Whenever you access an up value, Lua calls *luaF_findupval and that function does its job, however I’m a bit intrigued on when an up value list of a function is cleared / emptied.

In the code above, you can call b as many times as you want and it will still access the up value from it, from this, I’m assuming that the up value list of a function is cleared / emptied when it is garbage collected, am I getting this right?

local a = 5

local function b()
       a += 5
end

b()

Yes. Upvalues are stored on a list attached to the closure (the individual instance of that function). When the closure is collected, the upvalue references are too.

The values aren’t “allocated” in the way you’d imagine a table or function is. When open, the upvalue is just a reference to the actual variable in the stack. When it is closed, the upvalue gets stored in a closure and deduplicated across any other closures that refer to it.

2 Likes

Everything I say in this article was tested by me in various ways. I am not using certain terminology in order to make the article less confusing. This article isn’t written for people who already know how the garbage collector or lua internals work, its written for a range of people. If you’re just concerned about me using terminology inaccurately then understand that that’d make this almost impossible to serve its purpose.

Additionally, a lot of it I checked and found information on in various places on the devforum, for example, that do creates a new scope. You can find lots of people saying that do blocks create a new scope, and, as far as I am concerned luau/Roblox lua is a black box, the only thing that I care about (unless its stated by someone who works on the engine and would be able to verify directly) is what can be proven through testing so I don’t find it inaccurate to say that do creates a new scope if I can’t find a case that proves otherwise.

It might be that the data is allocated in the top scope but do still effects how data is garbage collected like any other scope from my testing. Are your tests failing because you are expecting the garbage collector to instantly collect after the scope exits?

This shows that do behaves in the garbage collector like a real scope. The variable in the do block will be garbage collected but the outer variable will not be garbage collected:

local topScope = {abc = 123}
local accessor = setmetatable({topScope}, {
	__mode = "kv"
})
do
	local lowerScope = {cde = 234}
	local accessor = setmetatable({lowerScope}, {
		__mode = "kv"
	})
	
	-- Spawn a new thread so we can watch the inner scope
	coroutine.resume(coroutine.create(function()
		wait()

		while wait(1) do
			print("In do block:", accessor[1])
		end
	end))
end

-- Spawn a new thread so we can watch the top scope
coroutine.resume(coroutine.create(function()
	wait()
	
	while wait(1) do
		print("Top scope:", accessor[1])
	end
end))

-- Do not let this thread die
--  I did it like this instead of using a loop to prevent the creation of a new scope
--  Creating a new scope wouldn't matter but it demonstrates that that can't be the cause this GC
wait(1)
wait(1)
wait(1)
wait(1)
wait(1)
wait(1)
wait(1)
wait(1)
wait(1)
wait(1)
print("Exit")

I did not describe this as upvalues because I am not writing for people who already know the terminology.

Closures do definitely hold reference to their upvalues. You can test this like so by switching the TEST_CASE variable.

You should see that if TEST_CASE is true (the abc function is deleted) the values are garbage collected. If TEST_CASE is false (the abc function’s reference is held) the values are no longer garbage collected:

local TEST_CASE = true -- If this is true the closure is niled

local function test()
	local someData = {abc = 123}
	local function abc()
		return someData
	end
	
	-- This table is not holding a reference to someData because of its weak values
	local accessor = setmetatable({someData}, {__mode = "v"})
	return abc, accessor
end

-- Abc and accessor are strongly referenced
local abc, accessor = test()

-- If using the test case, get rid of the abc variable
if TEST_CASE then
	abc = nil
end

-- Closure has finished running

-- Spawn a new thread (and let this one exit)
coroutine.resume(coroutine.create(function()
	wait()
	
	while wait(1) do
		print(abc, accessor[1])
	end
end))

The way I am even testing this actually relies on closures holding on to upvalues because if that were not the case the values would GC inside of the spawned thread and an error would happen.

I ran both of these tests and in both cases the results were consistent with what I expect and consistent with what I said in the article

3 Likes

I didn’t say it must be correct, or that it is completely accurate. I just said there is no reason for me to go “oh this might be false!” when all evidence I found points towards the thing being correct. What am I supposed to do in that case? I didn’t say you were wrong. All I said was that everything I found pointed towards one answer, therefore I had no reason to believe otherwise, and it doesn’t make a difference whether you are right about the specific implementation because another implementation which works how I described would be indistinguishable.

Luau and lua are very different so the lua source doesn’t really mean a whole lot to me because I treat the engine like a black box, I don’t care about what the lua source says, unless an engineer who works on luau who will know the answer says otherwise. Again, my methodology has been to test and if I can’t come up with a test case that contradicts it then there is nothing to say its wrong. I’m not just going to go “this might still be wrong” for every single thing I say, and I didn’t say “I am completely right no matter what this information is completely factual” the whole point of this tutorial is to create a clearer idea for people how the garbage collector works.

The point is not to give people every little technical detail and quirk that exists in the engine and be super technical and accurate.

What were you making a correction on? In the comment
from your example code you say that it doesn’t hold a reference. Are you just correcting my use of the terminology? I don’t really understand what the point of the last part of your message was or what you are trying to say. What about the text you quoted from my post is inaccurate?

Functions which reference external variables do hold references to those variables which is what I was showing, so what are you trying to correct?

I know how weak tables work, this is literally what I am using to test if a value gets collected. What am I confusing with a weak table? I am not confusing anything with a weak table, I am using a weak table to show the behaviour of the variable. The variable in the do block gets garbage collected, because there are no references to it. That was the whole point of the weak table, to show that the variable is getting garbage collected. If the value GCs, it becomes nil in the weak table, meaning the value of the variable must also be nil. This is what I am testing for and this is the whole point of using the weak tables.

I can’t test for values getting garbage collected if I can’t access them, so, what am I doing here that you think is wrong?

I don’t know what you were trying to correct me on.

2 Likes