Objects with no more references are not immediately garbage collectable with GC.collect(), however an intermediate call of e.g. new, writeln or Thread.sleep will make the unreferenced object reachable with GC.collect().
import std.stdio;
import core.thread;
import core.memory;
class C
{
string m_str;
this(string s) {this.m_str = s;}
~this() { writeln("Destructor: ",this.m_str); }
}
void main()
{
{
C c1 = new C("c1");
}
{
C c2 = new C("c2");
}
//writeln("Adding this writeln means c2 gets destructed at first GC.collect.");
//Thread.sleep( 1 ); // Similarly this call means c2 gets destructed at first GC.collect.
//int x=0; for (int i=0; i<1000_000_000;++i) x+=2*i; // Takes time, but does not make c2 get destructed at first GC.collect.
GC.collect();
writeln("Running second round GC.collect");
GC.collect();
writeln("Exiting...");
}
The above code returns:
Destructor: c1
Running second round GC.collect
Destructor: c2
Exiting…
Can anyone explain this reachability of objects during garbage collection?
I’m not familiar with the details of D’s garbage collection, but the general technique is to start from all “root pointers” to figure out which objects are live. When things are compiled down to machine code, this means starting from the function call stack and the CPU registers.
The code above might compile down to something like:
When the first
GC.collect()there are no more references to the first object (since$r0was overwritten). And even though the second object isn’t used, there’s still a pointer to it in$r0, so the GC conservatively assumes it’s reachable. Note that the compiler could conceivably clear out$r0after variablec2goes out of scope, but this would make the code run slower.When the first
writelncall executes, it probably uses register$r0internally and so it clears the reference to the second object. That’s why the second object is reclaimed after the second call toGC.collect().