Question about Memory Leak

If I have a code for the playerAdded event and characterAdded event, does that consider as a “memory leak”?

local Players = game:GetService("Players)

Players.PlayerAdded:Connect(function(plr)
plr.CharacterAdded:Connect(function(char)

end)
end)

Also, all I know how to stop from leaking memory is by disconnecting events.

1 Like

No, because you need to call this each time a player spawns.

Correct, connections can be either manually disconnected (:Disconnect(), .Connected → false). Moreover, the signal is going to disconnect once the object it is bound to is destroyed.

Consider the summarized example
local numberValue = Instance.new("NumberValue")

local connection; connection = numberValue.Changed:Connect(function(newValue)
	print(newValue)
end)

task.spawn(function()
	while numberValue do
		numberValue.Value += 1
		task.wait(1)
	end
end)

task.wait(1)
numberValue:Destroy()
print(connection.Connected) --> false

(Side note: the reference to number value is not lost yet - number value base is still defined - so it won’t be subject to garbage collection until we set numberValue to nil and thus lose all (strong) reference.)

The CharacterAdded will get disconnected once Player instance is properly destroyed. Does the engine remove it automatically? It should, but to be on the safe side, I hook something like the following to my PlayerRemoving connections.

game:GetService("Players").PlayerRemoving:Connect(function(player)
	-- save data
	-- destroy character when you don't need it anymore
	task.wait(100) -- a reasonable wait time
	player:Destroy()
end)

A useful announcement post regarding player:Destroy().

A small discussion other devs and I had regarding player and character removal, espeically character.



Some additional examples with characte resetting.

NumberValue outside character, not destroyed nor disconnected.
local PlayerService = game:GetService("Players")

local numberValues = {
	Instance.new("NumberValue"), Instance.new("NumberValue")
}

task.spawn(function()
	while true do
		task.wait(1)
		numberValues[1].Value += 1
		numberValues[2].Value += 10
	end
end)

PlayerService.PlayerAdded:Connect(function(player)
	local n = 0
	
	player.CharacterAdded:Connect(function(character)
		n += 1
		if n > 2 then return; end
		
		numberValues[n].Changed:Connect(function(newValue)
			print(newValue)
		end)
	end)
end)
NumberValue inside character, with commented code that destorys the character.
local PlayerService = game:GetService("Players")

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

PlayerService.PlayerAdded:Connect(function(player)
	local n = 0
	
	player.CharacterAdded:Connect(function(character)
		n += 1; local _n = n -- a private copy of n
		
		weakReferences[_n] = Instance.new("NumberValue")
		weakReferences[_n].Parent = character
		
		weakReferences[_n].Changed:Connect(function(newValue)
			print(newValue)
		end)
		
		while weakReferences[_n] do
			weakReferences[_n].Value += 1
			task.wait(1)
		end
	end)
	
	--player.CharacterRemoving:Connect(function(character)
	--	character:Destroy()
	--end)
end)

In the second example, you’ll notice how .Changed isn’t getting disconnected, which is still quite a strange phenomenon, compared to the very first example at the top of the post. Uncommenting the commented lines to eventually destory the character yields the expected result.

3 Likes

Last time I checked, Players are still not destroyed when they leave, meaning all their connections stay connected even when they leave the game. Of course, if a circular reference is present in one of these connections, the Player will not GC.

Essentially what I do to avoid this is just destroy the player when they leave using task.defer (Or you could just directly destroy the Player if your using Deferred SignalBehavior).

Players.PlayerRemoving:Connect(function(Player)
	task.defer(game.Destroy, Player)
end)

I haven’t tested extensively on how this would fair with systems that require working with the player after death, but I couldn’t imagine why it would cause problems.

2 Likes

The code you provided does not seem to have any obvious memory leaks. However, it is important to note that memory leaks can occur when event handlers are not properly disconnected. When an event handler is subscribed, the publisher of the event holds a reference to the subscriber via the event handler delegate. If the publisher lives longer than the subscriber, then it will keep the subscriber alive even when there are no other references to the subscriber . To avoid this issue, it is important to unsubscribe from events when they are no longer needed. This can be done by using the -= syntax to remove the event handler

@acaba1806 with all due respect, your response reminds of something AI would generate without enough context. Please refrain from copy-pasting AI replies. Tools like ChatGPT are amazing and can definitely provide a very valuable assistance, but at the time-being they still have to be verified. Especially the last sentence makes no sense and may raise additional confusion.

4 Likes

Uh I don’t actually use gpt chat lol I just like to help other developers

1 Like

How do we know if our code leaks memory?

A very obvious memory leak in connections happens when there’s a connection inside another one (which is frequently called). This is actually an extremely common case in beginners.

Say you have this:

Tool.Equipped:Connect(function()
	Tool.Unequipped:Connect(function()
	end)
end)

This would look okay to a beginner, but anyone who knows knows. Every time you equip the tool, it creates a new connection for when it unequips. You equip the tool again, a second connection! And this goes indefinitely because connections don’t get GCed and is actively being used.

If you notice your memory going up and not being cleared, open the Developer Console, in Memory → Place memory → LuaHeap (memory used by Lua), check if the memory continuously goes up and does not get released (back down). Common memory leak symptom. If there’s no signs of leaks caused by connections, you might want to check if there’s anything else being created (like Instances) but stay in the memory. Usually because their parent are set to nil, but they still exist.

CharacterAdded inside PlayerAdded shouldn’t be a major problem in smaller games. However because :Destroy() clears all connections, it’s recommended to destroy the player upon leaving, or manually disconnect it via :Disconnect(), cause apparently player doesn’t get destroyed on leave…

3 Likes

So all connections are disconnected when the instance is destroyed. If a connection keeps running and not disconnected, is it also a memory leak?

Also when a player leaves a game, why do we destroy the player object and not their character too?

Yes.

If the script “forgets” about the connection and moves on, doesn’t properly disconnect it or properly destroy the instance, it may be classified as a memory leak. In the example @Vanniris showed, the tool.Unequipped connection keeps being created on each equip. That’s most certainly a memory leak. By the time the tool is removed and destroyed, there may be dozens if not hundreds of useless unreachable connections.

I suggest taking a look at the tread I attached in my first post.

MarmDev, sorry for another quote ping. :slight_smile:

2 Likes

Btw I still don’t get it why the CharacterAdded inside the PlayerAdded does not consider as a memory leak.

Every time a player joins, a character is added?

Is it because different player, different character being added?

.CharacterAdded is connected a single time for every player. It calls the function and passes the fresh character on each (re)spawn. Because it’s bound to the player instance, it will get disconnected once that instance is disconnected and destroyed, which should happen internally. To avoid doubt, I wait a short amount of time for player to leave, and then call :Destroy() on the player. Should the player happen to rejoin, a new player instance will be created.

In case for some reason the engine doesn’t destory the player internally (after some time maybe?), and we don’t act ourselves, that would indeed counts as a memory leak.

1 Like

Say I have this code:

game.Players.PlayerAdded:Connect(function(player)
	local character = player.Character or player.CharacterAdded:Wait()
	print(character.PrimaryPart.CFrame)
end)

Will initializing character as nil in the end of the connection free up memory?

No need to do anything here :blush:, there’s no .CharacterAdded() connection in your code. :Wait() is a different story, at it will simply halt the execution of the code until character is created. That function will only run once.

No, I’m talking about the character variable, not the connection to it. I read somewhere that you should always initialize local variables inside connections/functions as nil at the end of your code. Is this true?

1 Like

If you are working in a limited scope - do-blocks, functions and similar - the variable will only exist inside. It doesn’t exist globally. So once whatever limited scope ends, the variable, unless referenced somewhere outside the scrope, is going to eventually be released from memory.

-- Values in this table hold reference, but not strong enough
-- to stop garbage collection.
local weakReference = setmetatable({}, {__mode = "v"})

do -- start of the scope
	local tabl = {}
	weakReference[1] = tabl
end -- end of the scope

-- Stimulate garbage collection.
game:GetService("RunService").Stepped:Connect(function(dt)
	local part = Instance.new("Part"); part.Parent = workspace
	task.wait(1); part:Destroy()
end)

repeat task.wait() until weakReference[1] == nil
print("Out of reach at this point.")

Which means no, you don’t have to set to nil, unless it’s a way to lose reference.

Also important: On instances, you could lose reference in your script, but as long as the parts was not destroyed, it would remain there.

1 Like

Do not worry, my friend.

Spreading resources and accurate information is the upmost important factor when learning.

2 Likes

Ok so basically players’ characters will get destroyed when players leave the game.

if the players never leave the game, their character won’t get destroyed nor their player.

This will make your game lag, memory leak?

If like new players keep joining.

The following is my observation based on my tests and the above posts.

  1. In my tests, for some reason, the old player’s character didn’t get destroyed after respawn. I made sure no strong reference was kept, and yet it remained in memory after a significant amount of time. I’ve seen some related bug reports. Perhaps the situation is going to change in the future.

→ In order to surely clear the old character (because each respawn a new one is created), it appears character:Destroy() yields the expected results.

  1. CharacterAdded is connected once per each player. Each time a new character is spawned for the player, it’s going to call the function, moments before the model appears in workspace. As said numerous times by now, connections end either by 1) manual disconnection or 2) removal of the instance they are connected to. I observed the CharacterAdded connection in studio and in game with (at least) two players. CharacterAdded remained connected after the player had left. I also had a piece of code to stimulate garbage collection.

→ One would expect player instance to eventually be destroyed. Nonetheless, to be on the safe side, we can also wait a small amount of time after the player has left, and destroy the instance. Following that, CharacterAdded disconnects as well.

1 Like