Setting tables to nil doesn't GC them?

I won’t post the code unless necessary - approximately 700+ lines to read through - but I’m currently testing a localscript chunk by chunk to isolate a source of memory leaks. I have a table filled with functions and I manually set that table to nil whenever my character dies (the hume.Died connection should be disconnected whenever my character is destroyed, I assume) and yet the table and its functions linger in memory long after my character respawns. Does setting the variable holding the table to ‘nil’ not properly allow the table to be garbage-collected? The reference to the table isn’t held anywhere else.

1 Like

You may have strong references in the table which are causing your leak and preventing the table from being garbage collected. What is your table comprised of? Furthermore, are you absolutely sure that you hold no references to the table itself?

2 Likes

I’m almost positive, yeah.

The table, as mentioned, is simply a dictionary of functions stored under a key - attacks that each class uses.

actions.Warrior = {
	Normal = function()
		local ind = atkIndex
		local targets = {}
		
		local index = {anims.slashA, anims.slashB, anims.slashC}
		local size = {v3(4, 2, 5), v3(6, 2, 4), v3(3, 2, 7)}
		local anim = index[ind]
		
		local con = nil
		
		con = anim:GetMarkerReachedSignal("Hit"):connect(function()
			local pos = hrp.CFrame + hrp.CFrame.lookVector * 3
			if ind >= #size then pos = pos + pos.lookVector * 1 end
			
			effect("sound", {id = 1112042117, p = 1.5, pos = hrp}, common)
			
			for _,targ in pairs(hitbox(pos, size[ind])) do
				targets[#targets+1] = targ
			end
			
			con:Disconnect()
			send:FireServer("Normal", {cf = pos, list = targets, size = size[ind], ind = ind})
		end)
		
		if atkIndex + 1 > #index then atkIndex = 1
			else atkIndex = atkIndex + 1
		end
		
		anim:Play()
		anim.Stopped:Wait()
		con:Disconnect()
	end,
	
	UNormal = function()
		local ind = atkIndex
		local targets = {}
		
		local index = {anims.spearA, anims.spearB, anims.spearC}
		local size = {v3(4, 4, 6), v3(7, 2, 5), v3(4, 2, 8)}
		local anim = index[ind]
		
		local con = nil
		
		con = anim:GetMarkerReachedSignal("Hit"):connect(function()
			local pos = hrp.CFrame + hrp.CFrame.lookVector * 4
			if ind >= #size then pos = pos + pos.lookVector * 1 end
			
			effect("sound", {id = 1112042117, p = 1, pos = hrp}, common)
			
			for _,targ in pairs(hitbox(pos, size[ind])) do
				targets[#targets+1] = targ
			end
			
			con:Disconnect()
			send:FireServer("Normal", {cf = pos, list = targets, size = size[ind], ind = ind})
		end)
		
		if atkIndex + 1 > #index then atkIndex = 1
			else atkIndex = atkIndex + 1
		end
		
		anim:Play()
		anim.Stopped:Wait()
		con:Disconnect()
	end,
	
	Charged = function()
		local size = v3(10, 2, 10)
		local anim = anims.WChargeA
		local spin = anims.WSpin
		
		local ct = 1.5; chargeID = rdm(1, 9999999)
		local this = chargeID
		local now = tick()
		
		anim:Play(); delay(ct - .1, function() 
			if anim.IsPlaying and chargeID == this then 
				anim:AdjustSpeed(0) 
			end 
		end)
		
		effect("chargecircle", {id = chargeID, part = hrp, ct = ct, rad = 12, col = rgb(255, 255, 0)}, common)
		
		while UIS:IsKeyDown(kc.E) do swait() end
		
		local perc = clamp((tick()-now) / ct, 0.1, 1)
		perc = ceil(perc * 100)
		
		anim:Stop(); spin:Play()
		effect("sound", {id = 12222208, pos = hrp}, common)
		effect("sound", {id = 220834000, p = .8, pos = hrp}, common)
		effect("endloop", {id = chargeID}, common)
		
		for i = 1, 3 do local pos = hrp.CFrame; local targets = {}
			for _,targ in pairs(hitbox(pos, size)) do
				targets[#targets+1] = targ
			end
			
			send:FireServer("Charged", {cf = pos, list = targets, size = size, perc = perc})
			wait(.1)
		end
		
		spin.Stopped:Wait()
	end,
	
	UCharged = function()
		local hitdb = {}
		
		local size = v3(3, 3, 8)
		local anim = anims.WChargeB
		local lunge = anims.WLunge
		
		local ct = 1.5; chargeID = rdm(1, 9999999)
		local this = chargeID
		local now = tick()
		
		anim:Play(); delay(ct - .1, function() 
			if anim.IsPlaying and chargeID == this then 
			end 
		end)
		
		effect("chargecircle", {id = chargeID, part = hrp, ct = ct, rad = 10, col = rgb(255, 255, 0)}, common)
		
		while UIS:IsKeyDown(kc.E) do swait() end
		
		local perc = clamp((tick()-now) / ct, 0.1, 1)
		perc = ceil(perc * 100)
		
		anim:Stop(); lunge:Play(); hume.AutoRotate = false
		
		effect("sound", {id = 12222208, pos = hrp}, common)
		effect("endloop", {id = chargeID}, common)
		
		local bv = i_n("BodyVelocity", hrp)
		bv.MaxForce = v3(5*10^4, 0, 5*10^4)
		bv.Velocity = hrp.CFrame.lookVector * (30 + (90 * perc/100))
		game:GetService("Debris"):AddItem(bv, .3 + (.1 * perc/100))
		
		for i = 1, floor(5 + (2 * perc/100)) do local targets = {}
			local pos = hrp.CFrame + hrp.CFrame.lookVector * 4
			for _,targ in pairs(hitbox(pos, size)) do
				if not hitdb[targ] then hitdb[targ] = true
					targets[#targets+1] = targ
				end
			end
			
			send:FireServer("Charged", {cf = pos, list = targets, size = size, perc = perc})
			wait(.06)
		end
			
		lunge:Stop()
		hume.AutoRotate = true
	end,
	
	Block = function()
		send:FireServer("Block")
		
		while UIS:IsKeyDown(kc.Q) do
			local state = hume:GetState()
			
			if state == hst.Jumping or state == hst.Freefall then break 
			else swait()
			end
		end
		
		send:FireServer("NoBlock")
	end
}

Warrior is just one of the five classes - I’ll post the rest as requested.

There shouldn’t be any strong references in the functions - what I’ve learned from scouring the forums and doing my own research is that (apparently) variables that are localized to a scope no longer hold reference once that scope ends I.E. the function finishes executing.

This is the connection that clears the ‘actions’ table. It also breaks all external connections, and testing has shown that I’m not failing to break them - if I run the code without any entries in the ‘action’ table by just commenting them all out, I experience no leaks.

hume.Died:Connect(function()
	for _,con in pairs(cons) do con:Disconnect() end
	cons = nil; anims = nil; actions = nil
	ping.OnClientInvoke = nil
end)

I know nothing about garbage collection in Roblox, but would recursively nullifying every value in the table before nullifying the table itself work?
Something like:

function clearTable(list)
	for k, v in pairs(list) do
		if (type(v) == "table") then
			clearTable(v)
		end
		list[k] = nil
	end
end

Can you elaborate on this? I don’t think a table can self reference itself once set to nil. I understand any complications that may arise if the table is a metatable but otherwise I don’t understand what this means, mind explaining?