When do variables cause memory leaks?

Up to now, i haven’t done anything with my variables after i’m done using them.
But i learned that this may cause memory leaks in certain situations, people have given me examples but i don’t understand. Can someone explain?

I’ve heard the terms “weak” and “strong” tables/variables but i don’t know what that means, i looked it up and it has something to do with metatables apparently, which made me more confused.

1 Like

i dont think variables can cause memory leaks,leaks only happen when u set a variable to a connection after used and u forget to clean it

Lua is a garbage collected language, so you really don’t have to worry about memory leaks like you do in other lower level languages.
As long as you’re doing normal things you don’t have to worry, once you stop using a table it will be collected automatically once there are no more references to it.

Roblox instances however are something you have to worry about, if you create instances make sure to use :Destroy() when you’re done with them, since otherwise they will be a memory leak, since roblox maintains references to those internally.

Here is a handy page on the forum that covers exactly this: A Beginner’s Guide to Lua Garbage Collection - Resources / Community Tutorials - Developer Forum | Roblox

1 Like

Roblox’s luau has an implemented garbage collector, for which you don’t need to worry about. Java for example has that aswell (if you’re familiar).
In Roblox luau you don’t need to worry, like you would need to in C. In C for example, you use malloc, to allocate memory for certain variables and you have to close and clean everything yourself.
The only problem in studio, is when you create too much instances, that can cause lag.
You can help prevent lag, doing the right use of “Debris Service”.

Check this problem i had in another thread

TLDR, It basically goes like this

local Function = function(Var)

	while Var do -- Loop never stops

		wait(1)
		print(Var) -- Always prints Part

	end

end

local Part = Instance.new("Part", workspace)

task.spawn(function()
	
	Function(Part)
	
end)

wait(2)

Part:Destroy() -- Like i mentioned before, the loop keeps going and Part doesn't change

@Gamer539099
My only conclusion is that sometimes Debris Service and :Destroy() aren’t enough, the variable doesn’t update to nil after Destroying their Instance.

It’s due to var having a so called “Pointer” on the “Part”, so it just points towards the address of the part which doesn’t get deleted on runtime. I’m not 100% sure how roblox memory addressing works, but it’s most likely cleaned memory ports after you stop running the game.
A pointer essentially points on a object in this case. This object occupies a certain amount of memory and has a memory address which gets “freed” when you stop running the game, even tough you destroyed it.

As far as I understand it the GC will only free up the memory once no reference to it exists, but the function now holds a reference to the part, so the GC won’t collect it.
Destroy and debris set parents to nil and lock the properties, meaning you can’t create new references to the part, but any existing references outside the scope of Destroy won’t be removed.

I think in the example, attempting to find the part or access a property of it should then return nil (or error!?) which would then end the loop.

1 Like

That is true, though they might still have internal references so to really clean them up for good you have to stop referencing them in code as well as call :Destroy()

Yes if you were talking about Luau’s cpp underside; however, in Lua you can just refer to it as a reference- because Lua(u) doesn’t have pointers and therefor it shouldn’t be referred to as such (Luau does have weak and strong references, which is much more idiomatic to Lua programmers than pointer may be).

Sigma is correct though. Here you run into a GC wall, garbage collection cannot know if you are finished or not with a destroyed object if you are maintaining reference to it in a separate thread.

The reference Var in the loop will persist the object as a strong reference. When the loop ends, and the incremental collector reaches its next marking stage, the object would then be marked for collection. As is when you destroy the object it will only become unparented (Roblox knows you’re done with it, but GC may not know until you discard your remaining reference). Basically, until the loop ends, the object will exist but entirely outside of the DataModel.

2 Likes

So after i destroy an instance, i have to make sure there is no variables being used that reference it? Is that it?

local Function = function(Var)
	while Var.Parent do 
		wait(1)
		print(Var)
	end
	-- Instance is destroyed, Part is locked and parentless, but still Var == Part
	Var = nil
	-- Part is destroyed, and no references to it, problem solved? does the GC service get rid of it now?
end

If it isn’t clear i’m having a lot of trouble figuring out when the GC service can collect my variables and when it isn’t able to.

  • Does it only happen because Var is being used in a loop?
  • Does it only happen because Var is an Instance?

Or does it only happen because Var is

  • An Instance
  • That happens to be in a While Loop
  • And the Loop happens to have a Var == true condition, meaning that the loop doesn’t end,
    Because Destroying the Instance doesn’t set Var to nil, because the Var is being referenced in the loop itself !?!?! That’s a paradox

So the same thing will happen with this code when you maintain a reference.

local Part = Instance.new("Part")
local Reference = Part

Part:Destroy()
print(Reference)

You are maintaining a strong reference to a destroyed userdata object; and therefor the object persists outside of the DataModel because the Luau collector has no idea if you’re finished with it or not. Typically you won’t run into this issue because you shouldn’t need a million 'weak reference scenario’s in Luau. You mostly need one reference to one object. The exception comes here, where the extra strong reference will not go out of scope for the near future.

Doing the following on an Instance userdata object will absolutely guarantee it gets marked for collection:

  • Nullify the Cpp side [:Destroy()]
  • Ensure all strong references are either set to nil, or the remaining strong references are expected to go out of scope in reasonable time (this could mean the script, loop, or thread ends as well).

In the code above, Reference will deallocate, but only because the script ends, at which point non-Cpp strong references are deallocated.

1 Like

Could you clarify the distinction between Strong and weak references? I’m not sure what you’re refering to. In your example are both Part and Reference strong? should i set both to nil?

When do variables go out of scope? I’m still unsure.

Other than this i think i understand what you’re saying.

Basically anything you can touch from your code, you can consider a strong reference, as the article I linked shows, you have to go out of your way to make weak references in lua.

Scope is in most cases just a block of code, it can be more complicated with closures but in most cases it will be straightforward.

-- Scope Begin
local a = {};
 b = {}

if true then
      -- Scope Begin
      local c = {}

     d = {}

    -- Scope End
end

print(c) -- nil, c went out of scope since it was local to the if block's scope
-- c gets collected after next gc cycle since there are no more references to it in scope

print(d) -- table, without the local keyword variables are defined in global scope

a = nil

b = nil

d = nil

-- Scope End

-- The tables referenced by a, b, and d are no longer referenced by their respective variables and will be collected.
1 Like

I explain both weak and strong references in my article which @RealistSophist linked above.

1 Like

Why did you set a to nil??? I read the guide i still dont get it.

a and b are in the same scope, being local to global scope is still being in global scope.

The example is kind of bad since all of that would be collected whether or not they were set to nil after the script ends, but if for example you had a while loop right after d = nil
the collector could collect the tables even though the variables are still in scope, since the variables don’t reference the tables anymore.

You shouldn’t ever be able to actually observe GC setting anything to nil without using weak references (there are almost no reasons to ever touch those in lua)

To put it in simple terms, I think it’s pretty safe to assume that if you can access an object from your script, it can’t be garbage collected.

Once you set a variable containing a table to nil, and you don’t have a reference to it saved somewhere else, there is no way you will be able to get it back without making a brand new table, therefor it is safe for GC to go ahead and clean it up.

Let me see if i understand.

All variables you make are strong, and they are made weak when out of scope. Which allows them to be collected by garbage collection

What stops it from going out of scope is:

A script, function, loop, etc having access to that variable. Once the script/function/loop ends, the variable goes out of scope, and is collected.

Is this correct?

Yes, that’s exactly how it works. Things get collected once there are no more references left to it.

function create()
      local tbl = {}

     return tbl
end

local a = create()

while wait() do end

The tbl variable here does go out of scope, but the function returns the reference it contains, which gets saved in a, so it can’t be collected until a is set to nil.
if you just called create() without saving the result however it would still be collected.

1 Like

Thank both of you very very very much for helping me with this one. Understanding that felt like trying to fit a square peg in a round hole.

Now this brings me back to the problem that made me find out about collection service in the first place: Variables that reference instances.

Unless i manually set them to nil, they will never be collected unless they are out of scope AND their instance is destroyed. Is this correct?

Read the comments in order?

local Function = function(Var)

	while Var.Parent do -- 2nd: Loop keeps this variable from being collected

		wait(1)

	end

	-- 5th: Var.Parent is nil, because the Part got destroyed, Var isn't nil tho, because it is inside this scope

end -- 6th: Scope ends, Var is nil?

local Part = Instance.new("Part", workspace) -- 1st: Variable references a Part

task.spawn(function()
	
	Function(Part)
	
end)

-- 3rd: Scope ends, but Variable doesn't get collected because of the function

-- in another script
workspace.Part:Destroy() -- 4th: Part gets destroyed

Part will indeed be collected.

  1. Part is created.
  2. Part is passed to a function as Var
  3. Part goes out of scope.
  4. Instance is destroyed.
  5. The loop ends since the parent is now nil.
  6. Function returns, so Var goes out of scope.
  7. There are no longer any active scopes with a reference to the instance, so the instance gets collected.
1 Like