Trying to understand memory leaks here

Right, so, the connection itself doesn’t generate its own thread, it’s part of the thread is was created in. So when the thread it was created in dies and the connection is disconnected, the connection itself should be GC’d as well.

Either way, I’m about to find out. I’m going to create a connection in a thread and terminate it, comparing with a coroutine. I’ll report findings.

1 Like

So you’re basically disagreeing with @egginabox0 answer, that is, when the connection is disconnected, it won’t be automatically garbage collected until con is set to nil to remove all references to the connection.

Which is going to be annoying because that means I have to set every variable to nil when I’m not using them anymore.

This is getting even more confusing, so how would I prevent memory leaks? Do I need to manually set all no longer used variables to nil? Or does the garbage collector handles it by itself?

You’re not exactly understanding what im saying.

The Connection is Disconnected, there wont be anymore threads, in which whatever is ran, will be garbage collected.
This only works if you destroy the Instance from which the connection is referencing are removed.

The function you are looking at with that feature is :Destroy(), as it removes itself from the reference so it can get garbage collected, which the connection does not have, meaning you’re required to do what I said:

If you talking about what the connection is firing, thats garbage collected, no need to worry about it.

If you’re talking about what its firing from, you have to manually diconnect it and remove it from the variable, or remove what its connected to. By diconnecting the connection, you prevented the memory leak, the last thing you need to do is dispose of the connection so that piece of information isn’t kept, so it also doesnt cause a memory leak.

Im not sure how I can explain this better, or make it less confusing, thats kind of my fault tbh.


He’s right, im not disagreeing with that, but there is an extra step to what needs to be done in order to dispose of the item.

Perhaps I didn’t give a clear example, so here’s another example:

Let’s say I have a lot of variables, and I won’t be needing all these variables once I am done using them.

The question is, do I need to set them to nil so that the garbage collector can collect it?

It really depends on whats there, i’ll try to explain.

If you have a group of Parts you can to remove, you can use the :Destroy() function, which sets the variable to nil, and the Parts are now gone from memory.

As for Connections, it doesnt have a :Destroy() function, because of that, you have to do it manually by setting it to nil.
So whenever something is being collected or checking to be collected, it checks if the object is “Dead” or “alive”, if its dead remove it, if its alive dont do anything to it. The connections Killswitch is :Disconnect(), and Boom, its Dead, so you now you dispose of it from the variable, and boom its now being garbage collected.

If you wanted to do the exact same process but with a Part, you just set its Parent to nil, then you set the variable to nil, you would be doing the exact same thing as you are with the connection, and :Destroy()


To make the process more managable, i generally just recommend adding them into a table, so a simple for loop can easily dispose of the objects.

local cons = {} -- a table
table.insert(cons, Part.Touched:Connect(function() print("e") end))
-- whatever amount of connections you want to add

for i,con in cons do -- loop through table
    con:Disconnect(); -- disconnect connection
    cons[i] = nil; -- dispose of connection
end

I also helps with organization, and I generally use it whenever im messing around with Players, such as connections like CharacterAdded, or Died, so im able to properly dispose of them.

Basically, its just one extra step to dispose of the object. Thats probably the best i can explain it.

1 Like

Not quite correct according to Creator Hub…

And this is what it has to say:

Tip: After calling Destroy on an object, set any variables referencing the object (or its descendants) to nil. This prevents your code from accessing anything to do with the object.

I’m super confused now. @DasKairo

1 Like

It says this in the documentation:

Sets the Instance.Parent property to nil, locks the Instance.Parent property, disconnects all connections, and calls Destroy on all children.

It then gives you a tip as to what else it does, which its basically saying that it removes any variables to the Object, and then removes its descendants. (if declared in a variable)

An object parented to nil is where it goes to be garbage collected, thats why we remove references and set parent to nil.

It also mentions memory leaks:

Disposing of unneeded objects is important, since unnecessary objects and connections in a place use up memory (this is called a memory leak) which can lead to serious performance issues over time.

1 Like

Alright, I think I understand now. Now the only problem left is, how do I deal with memory leaks in a big script, and where to find them…

Hilariously, we’re all wrong. @DasKairo @egginabox0

image

So the coroutine isn’t actually treated as its own thread. It’s still part of the thread it was generated in. So although the coroutine was running parallel to the current thread, when the current thread is killed (in this case, script destroyed), the coroutine is killed too.
image
Unsurprisingly, we see the same results for the script connection. When the thread dies, it does too.

And then, in the middle of typing this reply, I realized this proves nothing. We’ve already discussed and agreed on that.

OKAY FINE ILL CHECK THE MEMORY USAGE BEFORE AND AFTER I DISCONNECT THE SIGNAL

After all, this will be the ACTUAL test of whether or not only using :Disconnect() is a memory leak.

Oopsies, it is. Here’s how I think that.

First, I compared the typical LuaHeap memory usage while not doing anything. It consistently hovered around 234-240 MB after a minute. Next, it’s time to blow that number up like a balloon.

Using a MouseClick event from a clickdetector, I crammed about 3.5 million instances into a table over the course of a minute, causing the LuaHeap memory usage to steadily increase from the range listed above to about 300-310 MB. After 60 seconds, the script signal would be disconnected, where the ONLY reference to the table was wrapped in. Here’s what I did:
image
After 60 seconds, when the disconnection occurred, the memory usage did not decrease below the inflated range, staying at around 300-310 MB.

Alright, boys. That confirms it. The thread wasn’t GC’d as would be expected. Now let me try setting con to nil.

…And imagine my shock when setting con = nil after disconnecting didn’t clear the memory. It stayed at around 300-310 MB. I tried setting the table to nil too, and I got the same result.

What??? The memory isn’t being cleared at all, regardless of what I do? Am I doing this badly or implementing it wrong? Running Destroy on the part I was clicking and the script as well didn’t free the memory either. Did I just discover an engine bug while trying to win an argument on DevForums?

Tl;dr: Your code is cursed no matter what you do. Luau is god and it is not a merciful one.

That’s because the table a isn’t cleared right after the connection is GC’ed, so the table still contains memory of the instances. You’d need to clear the table by using table.clear(a).

This is fairly well known that coroutines and task (task may be an exception as it off a different system) in general, do not multithread. They run overtime with the main thread, as opposed to being completely independent.

If you’re looking for real multithreading, utilize more cores with Parallel Luau.

An Instance by itself does not hold a lot of memory, the memory leak is a problem that occurs overtime, meaning that more and more things are slowly added to memory, in which it will then increase, until it begins to lag.

Like with anything, it varies based on what your code is doing, and how it handles it. In which case you can see a gradual increase, or a slow increase.

A good rule of thumb is to just make sure you’re disposing of everything so this does not happen.

Doing that too and the memory usage doesn’t decrease.

The issue at hand here is that I am disposing of it. I’m setting everything I can to nil and calling Destroy on everything that could be correlated to allocating memory to this table. And even after doing so, the space in memory isn’t being freed.

The tip is telling you to do that yourself, not that the Destroy method does it.

1 Like

here’s a better example:

while true do
    Part.Touched:Connect(function()
       print("e");
    end)
    task.wait(1/30); -- to give some time to be able to use Studio lmao
end

Overtime, more connections will be added, and because they are not properly disposed, it will end up causing the game to lag just by moving around.

If you pay attention to the Developer Console on the Server side, you will see a slow increase of Memeory being used, typically around .003 MB. May not seem like much, but as mentioned, they occur overtime, where as his script is basically a really huge speed up in time of what could happen if something isnt done about it.

For most cases, you wont encounter much issues for a few minutes to a couple of hours. And as stated, It depends on what you’re doing, it can be gradually, or really slow. In which a good rule of them is to patch it before it becomes an issue.

I’m not referencing what I think a memory leak is. I’m pointing out that the engine clearly has a problem with freeing allocated memory when a thread is closed. I have the data numbers telling me exactly what’s happening. I’m adding data to a table, and when the thread referencing the table is closed, the memory isn’t being freed like you would expect it to. It’s not a question of “this is what’s gonna happen if something isn’t done about it”. You literally can’t do anything about it. I’d appreciate it if someone could try re-creating this with different variables and data types to see if it’s an issue specific to what I’m doing or if there’s generally just an issue with how the engine and Luau is handling the memory.

Thats because in your test, you are still holding all the data from every single run of the connection, The connection itself isnt the point of where its being stored, rather a, as it has any new references go there as opposed to the connection.

Lets take a run down of what you did:

--! A is declared prior to the connection
--! Connection is created
    --! Connection is fired
    --> New thread adds entries to A
    --> Thread is garbage collected

--! Yield 60 seconds
--> Connection is disconnected
--> A is not removed, therefore references still exist
--> Connection is not removed, therefore reference still exists

You are either not showing the entire code, or not properly disposing of the items thus not getting any memory freed.

Those entries dont disappear as soon as you disconnect the connection, they still existed in a after a run of multiple threads. Therefore the script is still using that memory, and not freed. Once they are being garbage collected (if at all), it will take time for the engine to process it all.

It is a matter of “this is what’s gonna happen if something isn’t done about it”, according to your logic, Its not my responsibility to manage information im using because I cant fix how my code manages information. You can fix all these issues for a more efficient outcomes, its called optimization.

Here’s a snippet of the entire script.

local a = {} -- add a heck of a lot of data into the table to inflate memory usage



local con = workspace.Part.ClickDetector.MouseClick:Connect(function()
	
	for i, v in pairs(game:GetDescendants()) do
		local count = 0
		while count < 10 do
			table.insert(a, v)
			count += 1
		end
		
	end
	print(#a)
end)

wait(60)
warn("Disconnecting")
con:Disconnect()
con = nil
table.clear(a)
a = nil
workspace.Part:Destroy()
script:Destroy()

As I previously stated, I am clearing the table, setting a to nil, setting the connection to nil after disconnecting, and calling Destroy on the part as well as the script. The issue persists. In my opinion, the issue is not how my code manages the information. Quite obviously, as there is no additional step I can take to attempt to clear it. If you feel that there’s something better I could be doing, then please send me a Message so that we can continue this conversation. I’d rather not flood this thread any more after it already having been marked as solved.

1 Like

Could it be that the Lua GC does not clean it up right away? I am not a pro in this field, but I remember reading about different priority levels for different Garbage collectors in different programming languages, so that some memory would take longer to get picked up and cleaned. Can this be what is happening? How long did you wait? Could it be that 300MB+ was deemed as “not that high” so the GC didnt bother to release the resources? Especially if your PC doesnt need RAM freed, I could see that maybe it was engineered to not clean some portions of memory if the computer didn’t need more memory. Just a hypothesis. Otherwise, your script seems perfectly logical, and if that script doesnt free memory, then I don’t know what will.

1 Like

Hey, some info here maybe my interpretation is wrong but instances will never get gced unless destroyed.
Metatables | Documentation - Roblox Creator Hub

I did some testing with similar code to yours and it seems to gc strings just fine.

for i = 1, 5 do
	local t = {}
	print(i)

	local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
		for _, inst in pairs(game:GetDescendants()) do
			for _ = 1, 50 do
				table.insert(t, inst.Name)
			end
		end
	end)

	task.wait(15)

	print(`Disconnecting with: {#t}`)
	con:Disconnect()
	con = nil
	t = nil
end
print("task done")

1 Like