A garbage collector is the part of a runtime environment that automatically reclaims memory once a program is finished with it. Most developers who write in Python, Java, Go, or JavaScript never manually free memory at all. The garbage collector does it for them. That convenience comes with trade-offs, and understanding those trade-offs helps you write faster, more predictable software.
Why memory management matters
Every program claims chunks of computer memory to store variables, objects, and data structures. When those chunks are no longer needed, the memory must be returned to the operating system (or the runtime) so something else can use it. Fail to do that, and you get a memory leak: the program keeps consuming RAM until the machine crawls or crashes.
In languages like C and C++, the programmer controls this manually. You call malloc() to claim memory and free() to release it. Get it wrong and you have either a leak or a dangling pointer. Both are painful. Garbage-collected languages remove that burden entirely, at the cost of some runtime overhead.
How a garbage collector actually works
There's no single approach. The two most common strategies are reference counting and tracing.
Reference counting tracks how many live references point to each object. When that count drops to zero, the object is dead and its memory is freed immediately. Python uses this as its primary strategy. It's simple and predictable, but it fails on circular references: two objects that point to each other never reach a count of zero, even after all external references are gone. Python supplements reference counting with a cycle-detecting collector to catch exactly this case.
Tracing garbage collectors take a different approach. They start from a set of "roots" (global variables, local variables on the call stack, CPU registers) and trace every reachable object. Anything unreachable is dead. The JVM (used by Java and Kotlin), the V8 engine (JavaScript), and the Go runtime all use tracing collectors. The most common variant is mark-and-sweep: first mark every live object, then sweep through memory and free everything that wasn't marked.
Generational collection
Tracing every object on every collection cycle is expensive. Generational collectors exploit a practical observation: most objects die young. A string created to format a log message lives for milliseconds. A database connection pool lives for the entire program.
Generational collectors divide the heap into at least two regions: the young generation and the old generation. New objects are allocated in the young generation and collected there frequently. Objects that survive enough collections get promoted to the old generation, which is collected less often. This concentrates expensive work where it's most productive. The JVM's G1 and ZGC collectors, V8's Orinoco, and .NET's CLR all use generational designs.
Stop-the-world pauses
The tricky part of garbage collection is that most algorithms need the heap to hold still while they work. If the program keeps allocating and modifying objects while the collector is tracing, objects can be missed or misclassified. The traditional solution is a stop-the-world pause: the runtime halts all application threads, runs the collection, then resumes.
Short pauses are unnoticeable. Pauses above 10 milliseconds start affecting interactive applications. Pauses above 100 milliseconds are visible as freezes or dropped frames. Modern collectors like ZGC (available in the JVM) and Go's concurrent collector do most of their work while the application runs, keeping pauses under a millisecond on typical workloads. This is one of the most active areas of runtime engineering today.
Garbage collection vs. manual memory management
Rust takes a third path: it enforces memory safety at compile time through a system of ownership and borrow rules, with no garbage collector and no manual free() calls. The compiler rejects code that would cause a use-after-free or a data race. The result is deterministic, low-latency memory management that suits systems programming. The cost is a steeper learning curve.
Garbage-collected runtimes trade peak performance for developer productivity. For most web services, mobile apps, and business software, that's the right trade. For real-time audio processing, kernel drivers, or game engines with tight frame budgets, it may not be.
What garbage collection doesn't fix
A garbage collector only reclaims memory that is truly unreachable. If your code holds a reference to an object it's no longer using (say, appending to a list that grows forever) the collector can't help. The object is still reachable. This is a logical leak, not a technical one, and it's the most common source of memory problems in garbage-collected programs.
Garbage collection also doesn't eliminate all concurrency bugs. Two threads can still read and write shared state in the wrong order. For that class of problem, you need a mutex or another synchronisation primitive to coordinate access.
Tuning and observability
Most production runtimes expose garbage collection metrics. The JVM prints GC logs with -Xlog:gc. Go exposes GC pause times through its runtime package. Node.js surfaces heap statistics via process.memoryUsage(). Watching these numbers is the fastest way to diagnose a performance problem that turns out to be collection pressure rather than algorithmic slowness.
Heap size matters too. A heap set too small forces frequent collections. A heap set far too large means each collection has more to scan and promotes objects to the old generation that would otherwise have died young. Finding the right size for a given workload is part tuning, part measurement, and part knowing your object lifetimes.
Garbage collection is one of the foundational ideas in modern software. Most programs running in Australia today, from banking apps to media streaming services, depend on it to stay stable under load. Understanding how it works tells you a great deal about where runtime slowdowns actually come from, and why fixing them sometimes requires more than rewriting an algorithm.

