local t={}
do
local checker=setmetatable({t},{__mode='v'})
coroutine.wrap(function()
while checker[1] do
print'exists'
wait()
end
print'gced'
end)()
end
local ev=workspace.RemoteEvent
ev.OnClientEvent:Connect(function()
_=t
end)
t.x=ev
ev.AncestryChanged:Wait()
print'c got kill'
Server side:
local ev=Instance.new("RemoteEvent",workspace)
wait(3)
ev:Destroy()
print("s destroyed")
Interestingly, if you manually disconnect the event on the client side when ancestry changes (bc no destroyed event ) it successfully gc-es
local t={}
do
local checker=setmetatable({t},{__mode='v'})
coroutine.wrap(function()
while checker[1] do
print'exists'
wait()
end
print'gced'
end)()
end
local ev=workspace.RemoteEvent
local disc=ev.OnClientEvent:Connect(function()
_=t
end)
t.x=ev
ev.AncestryChanged:Wait()
print'c got kill'
disc:Disconnect()
local checker=setmetatable({t},{__mode=‘v’}), this sets the metatable on an anonymous table that contains t, not on t
due to the way we bridge objects from the engine to lua, you cannot use weak table keys to establish if an instance is alive or dead, it is possible to get false positives (the key may leave the table before the instance has been deleted. The only reliable way to know if there is an instance leak is to check the instance count (game:GetService(“Stats”).InstanceCount)
There is a dependency cycle. The remote event will hold a reference to the callback for as long as the script thread that created the callback is still alive. The callback holds a strong reference to “t” for use in the callback. “t” holds a strong reference to the remote event (creating a cycle). This is why disconnecting the callback allows the object to be collected.
i’m not really checking if an instance is gced though, the ‘checker’ weak table has table t inside of it
the wiki for destroy explicitly states Instance:Destroy “disconnects all connections”
& as an elaboration for #3, the example I was talking about in the OP’s link is
do -- Also also all good, as Destroy() implicitly disconnects all connections
local p = Instance.new('Part')
p.Touched:connect(function() print(p) end)
p:Destroy()
end
which if you test for gc, it successfully disconnects&gces (as the wiki explains should happen)
another clarification:
I’m talking about the table not gcing, not the RemoteEvent instance
but as a consequence of the table not gcing and still holding a reference to the instance, [at least part of] the RemoteEvent instance won’t gc either
You cannot force a lua table to become gc’ed by putting in to some other table with a weak reference. All callbacks contain strong references to all closed variables, so
local disc=ev.OnClientEvent:Connect(function()
_=t
end)
holds a strong reference to t for as long as the callback exists. One other part that may be confusing here: due to legacy reasons, if an object is deleted on the server, we do not call :Destroy on it, we only call .Parent = nil, meaning that de-parenting on the server will not clean up connections for you.
In the last year we transitioned to a purely server-authoritative model; before that point, games were allowed to let clients perform significant game changes client side. In a heavily distributed environment like that, being more gentle with Parent = nil allowed for more flexibility.
Then, because we have maintained that behavior for so long up to this point, there is a lot of potential to break games if we change this behavior today. Some games specifically leverage network de-sync on client to create local effects, and those kinds of effects can break if we forcibly Destroy everything that the server releases. We try very hard to avoid breaking changes like that in general as a product directional thing.