GC: How Unreachable Objects Are Found in .NET, Java, Python
Garbage collection (GC) automatically reclaims memory occupied by objects your program no longer needs. The key question is: how does the runtime know an object is truly unreachable? This guide explains the tracing algorithms used by .NET, Java, and Python, and shows you how to reason about reachability in your own code.
What You’ll Need
- A basic understanding of references and object lifetimes in your chosen language.
- Access to a command line or IDE to run small experiments (optional but helpful).
- For .NET: the
dotnetCLI or Visual Studio. - For Java: the
javac/javatools or an IDE like IntelliJ. - For Python: any Python 3 interpreter.
Step-by-Step
- Understand the root set. Every GC starts from a set of objects that are always considered alive: local variables and parameters on active stack frames, static fields, global variables, and (in .NET and Java) objects referenced by the runtime itself (e.g., JNI handles, thread objects). In Python, the root set includes the module globals, the call stack, and objects referenced by the interpreter’s internals.
- Trace references from the roots. The GC walks every reference from each root object. For each referenced object, it marks it as reachable, then recursively follows that object’s references. This is a graph traversal (depth-first or breadth-first).
- Mark everything you visit. In .NET and Java, this is the mark phase: each object gets a bit set in its header. In Python, the reference cycle detector uses a different approach (see step 6), but the initial reachability tracing is similar.
- Identify unreachable objects. After the trace completes, any object that was not marked is unreachable. Its memory can be reclaimed. In .NET and Java, this is the sweep or compact phase. In Python, the reference counter drops to zero for objects that were only referenced by other unreachable objects, but cycles need special handling.
- Handle reference cycles. A cycle (A references B, B references A) can keep reference counts above zero even when no root points to the group. Java and .NET handle cycles naturally because they trace from roots, not by counting. Python’s reference counting alone would leak cycles, so it runs a separate cycle detector that periodically traces from roots to find unreachable cycles.
- Understand generational collection (optional). Most modern GCs divide objects into generations (young, old). The tracing algorithm is the same, but the root set for a young generation collection includes references from older generations to younger ones (remembered sets). This is an optimization, not a different reachability definition.
- Test your understanding. Write a small program that creates objects, drops references, and forces a GC. Use memory profiling tools (like
dotnet-counters,jvisualvm, or Python’sgcmodule) to see when objects are collected.
Common Mistakes
- Assuming nulling a variable always frees memory. If another reachable object still references the object, it stays alive.
- Forgetting about event handlers and delegates. In .NET and Java, a subscriber object can be kept alive by a publisher that holds a reference to the subscriber’s handler. This is a classic memory leak.
- Ignoring static fields. A static collection that you keep adding to never becomes unreachable, even if you stop using it.
- Thinking Python’s reference counting handles cycles. It doesn’t; the cycle detector is separate and runs periodically. If you create many cycles, memory usage can spike before the detector runs.
- Confusing unreachable with
null. An object can be non-null but unreachable, and vice versa.
Tips
- Use weak references (e.g.,
WeakReferencein .NET,WeakReferencein Java,weakrefin Python) when you want to cache an object without preventing its collection. - Implement
IDisposable(C#) orAutoCloseable(Java) for non-memory resources, but remember that the GC only handles memory, not file handles or sockets. - In Python, you can manually trigger the cycle detector with
gc.collect(), but it’s rarely needed in production. - Use memory profilers to see what’s actually kept alive. The GC’s view of reachability is the ground truth.
- Be mindful of large object heaps (LOH) in .NET; they are collected less frequently.
Troubleshooting
Memory grows even though you think objects are unreachable. Take a heap dump. In .NET use dotnet-dump, in Java use jmap, in Python use tracemalloc or objgraph. Look for unexpected references from static fields, event handlers, or caches.
OutOfMemoryException / MemoryError. This usually means the GC cannot reclaim enough memory because too many objects are reachable. Check for infinite caches, static collections, or objects that register themselves in a global list.
Objects are collected too early. If you’re using weak references and the object is collected while you still need it, you must keep a strong reference somewhere. This is a design issue, not a GC bug.
Finalizers not running. In .NET and Java, finalizers are non-deterministic. Don’t rely on them for cleanup. Use using (C#) or try-with-resources (Java) for deterministic disposal.
Python cycle detector doesn’t catch a cycle immediately. The detector runs on a threshold. If you need immediate collection, call gc.collect() after creating many cycles.
Frequently Asked Questions
Q: Does setting a variable to null guarantee the object is collected?
A: No. It only removes one reference. The object is collected only when no reachable references remain, and the GC runs.
Q: Can I force garbage collection?
A: Yes, but it’s rarely a good idea. In .NET use GC.Collect(), in Java System.gc(), in Python gc.collect(). These are hints, not guarantees, and overuse hurts performance.
Q: Why does Python use reference counting while Java and .NET do not?
A: Python’s reference counting makes collection immediate and simple, but it fails on cycles. Java and .NET use tracing, which handles cycles naturally at the cost of occasional pauses.
Q: What is the difference between reachable and live?
A: Reachable means a reference path exists from a root. Live usually means reachable and not yet finalized. In practice, they’re the same for GC purposes.
Q: Can an unreachable object become reachable again?
A: In some runtimes, a finalizer can resurrect an object by storing a reference to it in a static field. This is discouraged and can cause memory leaks.
Q: How do I find memory leaks in my application?
A: Take heap snapshots at two time points and compare which objects grew. Tools like Visual Studio Memory Profiler, Eclipse MAT, or Python’s pympler can help.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.