Object reachability: when an object can be collected
Trace which library objects a program can still reach from its roots, see why a cycle does not keep itself alive, and separate unreachable from released. Failure and resource contracts · Object-oriented programming.
A librarian clears the checkout screen, and Mira's membership card still turns up in the catalog. Nobody deleted it. The program removed one variable, and something else still pointed at the Member. Which objects can the program still reach, and which ones may the runtime take back?
Start from the roots
Every object lives in memory, and the program uses it by following references: a variable holds a reference, and an object's field can hold another. A root is a reference the runtime treats as a starting point, such as a static field, a variable or parameter in a method that is still running, or a reference held by a live thread. The root is the reference, not the object it points at. An object is reachable when you can walk from some root, reference by reference, and arrive at it. Java's specification puts it as an object a live thread could still use in the rest of its work.
Our library has a Catalog and a checkout request. Each is a root: the catalog is a static field, and the request is a parameter of a running method. Both point at Member Mira. Mira's Loan points at Book Dune. Watch each root's path through the chain.
Two roots, one Member
Read the steps as text
- The catalog and the request are both roots, and both point at Mira. Mira's Loan points at Dune.
- Root request is cleared; it pointed at Mira. The request returned. Mira lost one holder, but the catalog still points at her.
- A walk from catalog reaches Mira, Loan and Dune. All three are still reached, so none of them became eligible.
- A walk from every root reaches Mira, Loan and Dune. A path from some root decides, not how many variables were cleared.
static Catalog catalog = new Catalog(); // root: a static field
static void checkout(Member request) { // root: a parameter while this runs
request.current = new Loan(catalog.find("Dune")); // Mira -> Loan -> Dune
}
// call site: checkout(catalog.find("Mira")); // catalog -> Mira is a second pathcatalog = Catalog() # root: a module-level name
def checkout(request): # root: a parameter while this runs
request.current = Loan(catalog.find("Dune")) # Mira -> Loan -> Dune
# call site: checkout(catalog.find("Mira")) # catalog -> Mira is a second pathWhen checkout returns, the request root is gone. Mira is still reachable, because the catalog has a path to her, and so are her Loan and Dune. The deciding fact is whether a path exists, not how many variables were cleared.
Counting holders is not the test
It is tempting to say an object is gone once nothing points at it. That is the idea of a holder count: the number of references to the object. A count of zero does mean unreachable. The reverse fails. Keep Mira and her Loan from before, where Mira's current field points at the Loan. Now let the Loan point back: its member field is set to Mira. Dune sits on a shelf, a second root. Then the catalog root goes away.
static Catalog catalog = new Catalog(); // root 1: the only path to Mira
static Shelf shelf = new Shelf(); // root 2
catalog.add(mira); shelf.add(dune); // catalog -> Mira, shelf -> Dune
loan.member = mira; // the back-edge: Loan -> Mira, with Mira -> Loan already set
mira = null; loan = null; // the local variables let go too
catalog = null; // Mira <-> Loan: no root reaches the pair nowcatalog = Catalog() # root 1: the only path to Mira
shelf = Shelf() # root 2
catalog.add(mira); shelf.add(dune) # catalog -> Mira, shelf -> Dune
loan.member = mira # the back-edge: Loan -> Mira, with Mira -> Loan already set
mira = None; loan = None # the local variables let go too
catalog = None # Mira <-> Loan: no root reaches the pair nowEach object in the pair still has one holder: the other one. Yet no root has a path into the pair, so the program's code can no longer use either. The pair is an island, and a loop between its members does not tie it to any root. Dune stays reachable through the shelf.
A loop with no way in
Read the steps as text
- The catalog points at Mira, Mira's Loan points at Dune, and the shelf points at Dune. No Loan points back at Mira yet.
- Loan's member reference now points at Mira. Mira and the Loan now point at each other, and both are still reachable.
- Root catalog is cleared; it pointed at Mira. Each of the pair still has one holder, the other one.
- A walk from shelf reaches Dune. No root has a path into the pair, so both are unreachable. Dune is still on the shelf.
- A walk from every root reaches Dune. Unreachable only makes them eligible. Nothing here shows them being reclaimed.
- Root catalog is empty, so a walk from it reaches nothing. Nothing reaches the pair, so it stays eligible. When the runtime reclaims it is not a time to rely on.
A smaller cut works the same way. Cutting the one reference between Mira and her Loan makes the Loan unreachable only if no other root still reaches it. If it does become unreachable, everything reachable only through the Loan becomes unreachable too, as Dune does in this picture.
Cut one edge, not the object
Read the steps as text
- The catalog points at Mira and the request points at her Loan. Mira's current field also points at the Loan.
- Mira's current reference to Loan is cleared. The request still reaches the Loan, so nothing is eligible yet.
- Root request is cleared; it pointed at Loan. The Loan lost its last path, and Dune is only reachable through it.
- A walk from catalog reaches Mira. Mira is the only object a root still reaches.
Unreachable means may be reclaimed, not was reclaimed
Notice what the cycle scene never showed: a moment where the pair disappears. That is deliberate. Unreachable makes an object eligible for collection. The collector is the runtime part that reclaims memory from such objects, and the languages let it choose when.
In Java, the specification says storage of unreachable objects can be reclaimed, and it does not say when. System.gc() is a suggestion that the virtual machine spend effort recycling unused objects. The JVM may ignore it, and nothing promises that a particular object is gone or that a resource is closed. A program that needs something to happen at a certain moment cannot hang it on the collector. A local variable that is never used again also need not keep its object alive, so do not count it as a dependable root.
Python's language manual says unreachable objects may be garbage-collected and lets an implementation postpone collection or omit it. CPython, the common implementation, frees most objects when their reference count reaches zero and uses a separate cycle detector for loops. That is one implementation's behaviour, not a language promise.
System.gc(); // a suggestion: the JVM may or may not reclaim the unreachable pair nowimport gc
gc.collect() # CPython: runs a full collection and does collect an unreachable cycleIn Java, System.gc() does not tell you whether the pair is gone. In CPython, gc.collect() does collect that unreachable pair, but the manual does not promise this for other implementations, so do not build a design on it.
Reachability is not release
Suppose Mira's Loan holds an open lease on a reading-room seat, a resource managed outside your program's memory. The collector may eventually reclaim the Loan's memory. Nothing in reachability tells the reading room that your lease is finished. Java's finalization offers no promise about when or in what order finalizers run, and it is deprecated for removal since Java 18 (JEP 421), so it is no plan either. Python warns in the same spirit: __del__ is not guaranteed to run for objects that still exist when the interpreter exits.
Unreachable is not released
Read the steps as text
- The Loan holds a reading-room lease that is open. The catalog points at Mira, and Mira points at the Loan.
- Mira's current reference to Loan is cleared. Nothing reaches the Loan any more.
- A walk from catalog reaches Mira. The Loan is eligible, and its lease still reads open.
- A walk from every root reaches Mira. Closing the lease takes an explicit step that the cleanup lesson owns.
So the two questions stay separate. "Can the program still use this object?" is reachability, decided by roots and paths. "Has the lease been given back?" is a cleanup contract, and the owner has to honour it with an explicit close() or a block that closes it. That contract is the subject of resource ownership and cleanup. Becoming unreachable is a fact about memory. It is not a cleanup plan.
Recap
- An object is reachable when a path of references leads to it from a root; one cleared reference changes nothing while another path remains.
- A holder count above zero does not keep an object alive: a cycle with no root path is unreachable.
- Unreachable means eligible for collection. Java and Python both leave the timing to the implementation, and
System.gc()is a suggestion andgc.collect()is one runtime's behaviour; neither is a cleanup step. - Releasing a resource is a separate contract. Use an explicit close, not the collector.
Go deeper
Roots in Java include more than variables and static fields; live threads and other runtime-held references also count, and the specification defines reachability in terms of what a continuing computation can access. In Java, an explicit close (try-with-resources) is the replacement for finalization. Weak, soft and phantom references, Cleaner and Python's weakref.finalize let a program observe or react to collection, and they are not covered here. In CPython an acyclic object is freed when its last name goes, but a cycle waits for the cycle collector, which gc.disable() turns off.
You now know which objects a program still holds and which ones the runtime may take back. Next, exception contracts show how a method reports failure while keeping its objects valid. To practise following references from a starting point and marking what you reach, try Graph Fundamentals.