If you think of the objects in memory as a tree, the "roots" would be the root nodes - every object immediately accessible by your program.
Person p = new Person();
p.car = new Car(RED);
p.car.engine = new Engine();
p.car.horn = new AnnoyingHorn();
There are four objects; a person, a red car, its engine and horn. Draw the reference graph:
Person [p]
|
Car (red)
/ \
Engine AnnoyingHorn
And you'll end up with Person
at the "root" of the tree. It's live because it's referenced by a local variable, p
, which the program might use at any time to refer to the Person
object. This also goes for the other objects, through p.car
, p.car.engine
, etc.
Since Person
and all other objects recursively connected to it are live, there would be trouble if the GC collected them.
Consider, however, if the following is run after a while:
p.car = new Car(BLUE);
And redraw the graph:
Person [p]
|
Car (blue) Car (red)
/ \
Engine AnnoyingHorn
Now the Person
is accessible through p
and the blue car through p.car
, but there is no way the red car or its parts can ever be accessed again - they are not connected to a live root. They can be safely collected.
So it's really a matter of taking every starting point (every local variable, globals, statics, everything in other threads and stack frames) — every root — and recursively following all the references to make up a list of all the "live" objects: objects which are in use and unsuitable for deletion. Everything else is garbage, waiting to be collected.