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

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()
2 Likes

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.

6 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

5 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.

4 Likes

This is inaccurate. do-end creates a new scope just like if then-end and any other thing that opens a block. The local limit is per function not per scope, and please stop citing the Lua source as your source because spreading misinformation and citing the Lua source is going to confuse beginners into thinking you are right.

1 Like

I’m aware but that wasn’t my point regarding do-end. What I really meant was that do-end still shares the same stack, it doesn’t actually create a new one. If you allocate 199 variables, and then allocate 1 more variable in a do-end scope, you would exceed the local limit limit. Sorry for my bad wording there.

how fast, and how consistent is garbage collector between each collection?

1 Like

It will probably vary greatly on a case by case basis. I suggest trying the microprofiler and studio profiling tools to see if it’s affecting performance.

2 Likes

I think it’s nice to add that weak tables if used too much or with many values has some impact on performance, but you’re most likely to be just fine.

AFAIK, garbage collection happens every frame, somewhere in between RunService events, in that process it will clean up some of the values that have no strong references, and it will keep cleaning as cycles pass. Garbage collection doesn’t happen at all once.

The time that the object has existed seems to affect how long it takes to GC as well.

1 Like

If I do
a, b = 5, workspace.Part
table = {a, b}

table = nil

would it be the same as
table = {5, workspace.Part}
table = nil ?

Not really, you still have the two variables a and b that aren’t set to nil in the first snippet.

If it happens, is there a way to restore memory that was resetted like ingame data?

Garbage collection will not remove memory that is in use, garbage collection is about removing memory that is not in use. It means that if you get rid of a value completely, you can’t use that value anymore, so Roblox shouldn’t continue to reserve memory for it.

1 Like

Oh so if my game faced data loss, any way I can reverse the data?

If your game faced data loss it wouldn’t have anything to do with garbage collection. Memory & DataStores are two very different and unrelated things.

4 Likes

hello friend thanks for the amazing tips however due to my self-taught english I don’t quite get everything so I just got a few questions, script in startercharacterscripts:

local collectionService = game:GetService"CollectionService"


local character = script.Parent
local humanoid = character:WaitForChild"Humanoid"
local ObjectsToPrint = {}


humanoid.Died:Connect(function()
   for index,Value in pairs(ObjectsToPrint) do 
         Value = nil
         if ObjectsToPrint[Value] then 
            ObjectsToPrint[Value] = nil
         end
   end
   ObjectsToPrint = nil   
end)

local event  = Instance.new"BindableEvent"
event.Name = "Random"
event.Parent = character


event.Event:Connect(function(actionString, informationTable)
      if actionString == "AddTableObjects" then
          if collectionService:HasTag(character,"Printing"..informationTable.Name) then return end 
          collectionService:AddTag(character,"Printing"..informationTable.Name)

          if not collectionService:HasTag(character,"Printing") then 
                collectionService:AddTag(character,"Printing")
          end

            if not ObjectsToPrint(informationTable.Name) then 
                  ObjectsToPrint[informationTable.Name]= {}
            end 

            ObjectsToPrint[informationTable.Name].Word = informationTable.Word
            ObjectsToPrint[informationTable.Name].Delay = informationTable.Delay

            task.delay( informationTable.Delay , function()
                    local number = 0 
                    for index,Value in pairs(ObjectsToPrint)
                         number+=1
                    end 

                    print(ObjectsToPrint[informationTable.Name].Word)
                    ObjectsToPrint[informationTable.Name] = nil
                    number -= 1

                   collectionService:RemoveTag(character,"Printing"..informationTable.Name)

                   if number <= 0 then 
                         collectionService:RemoveTag(character,"Printing")
                         print"Printed everything in the table."
                   end 
            end)
     end
end
--other scripts
for i = 1,5 do
   character.Random:Fire("AddTableObjects", {Name = "Example_"..i, Word = "Hello_"..i , Delay = .5})
   task.wait(Random.new():NextInteger(1,5)
end

this is a poorly constructed example of a code i use, apologies for any mistakes or anything, writing here is weird, i use it for things such as “AddSpeed” to change humanoid walkspeeds depending on priorities and stuff…anyway is there anything else to be cleared here when the character dies? as much as I understand this is everything i need to clear right, set the table to nil again, correct me if im wrong please im getting scared this stuff is stacking i have about 12 of those tables and 12 of those if statements(1 if 11 elseifs) per character spawn as you can see below, they mostly have that same code base, for different things, jumppower, walkspeed, autorotate, ragdoll…etc.
image

if you ask why i use the external table in the example its because sometimes i need to remove something from the table before the delay is up, its different in my case i hope you can understand the analogy, thanks friend

I’ve tried to use this to test some cases but I don’t think I’m doing it correctly. Can you provide better examples of how this is used please?

The time at which things GC isn’t consistent, and isn’t gauranteed to change, so the only way to really utilize this is to poll it. It’s not practical or useful except for testing.

local ref = setmetatable({}, {__mode="v"})

local function isReferenced()
	return ref[1] -- ref[1] can be GCed and if it does this will be nil!
end

ref[1] = {someValue = 123}

task.spawn(function()
	-- Each frame, check if the value has GCed
	while isReferenced() do
		task.wait()
	end

	print("Value has GCed")
end)

i’m still learning garbage collection, would doing this be an issue (example)?

script.Parent.Event:Connect(function()
    local AllClear = true
    for _,plr in ipairs(Players) do
       if plr.Clear.Value == false then
          AllClear = false
          break
       end
    end
    if AllClear then
       -- other stuff
    end
end)

Testing GC in a ServerScript and pressing run does not seem to work