The short answer
Quick answer: Garbage collection (GC) is automatic memory management. Instead of the programmer freeing memory by hand, the language runtime periodically works out which objects the program can still reach and reclaims everything else. Most collectors do this by tracing: starting from a set of roots (global variables and local variables on the stack), following every reference, and treating anything not visited as garbage. To do that safely the collector sometimes has to pause the program briefly, which is where GC pauses come from.
Why automatic memory management exists
Programs constantly create objects on the heap. In languages like C, the programmer must release each one manually. Three classic bugs follow:
- Memory leak. Forgetting to free memory, so usage grows forever.
- Use after free. Freeing memory and then using it anyway, causing crashes or security holes.
- Double free. Freeing the same memory twice, corrupting the allocator.
A garbage collector removes the second and third entirely and most of the first. The price is some CPU time, extra memory and occasional pauses.
What counts as garbage
A collector cannot know whether you will use an object again. It uses a safe approximation: reachability. An object is alive if the program could possibly reach it by following references from a root.
Roots are:
- Local variables and parameters on every thread's stack.
- Global and static variables.
- CPU registers and a few runtime internals.
Anything not reachable from a root can never be used again, so it is safe to reclaim.
Mark and sweep
The simplest tracing algorithm, explained clearly in Crafting Interpreters, has two phases:
- Mark. Start at the roots. Follow every reference and mark each object you find. Continue until nothing new is found.
- Sweep. Walk the whole heap. Free every object that is not marked and clear the marks on the rest.
It handles cycles naturally: two objects that point at each other but are unreachable from the roots never get marked.
Plain mark and sweep leaves the heap fragmented, with free gaps scattered between live objects. Two refinements address this:
- Mark-compact slides live objects together, leaving one large free area.
- Copying collection copies live objects into a fresh region and discards the old region wholesale. Allocation afterwards is extremely fast, since it is just moving a pointer forward.
Reference counting
A different approach keeps a counter on every object recording how many references point to it. When the counter reaches zero, the object is freed immediately.
| Tracing GC | Reference counting | |
|---|---|---|
| When memory is freed | At the next collection | Immediately |
| Pauses | Possible | Mostly none, but freeing a large structure can cascade |
| Cycles | Handled | Not handled without extra help |
| Ongoing cost | Paid during collections | Paid on every reference change |
The weakness is cycles: if A references B and B references A, neither count ever reaches zero. CPython uses reference counting plus a separate cycle detector. Swift uses automatic reference counting and asks the programmer to mark some references as weak to break cycles.
The generational idea
Measurements of real programs show a strong pattern, often called the generational hypothesis: most objects die young. A temporary string or a short-lived request object is created and becomes garbage almost at once.
Generational collectors exploit this by splitting the heap:
- Young generation. New objects go here. It is small and collected often. Since most of its contents are already dead, a copying collection is very quick.
- Old generation. Objects that survive a few young collections are promoted. This area is collected rarely.
Young collections are frequent but cheap; full collections are expensive but rare. Java, .NET and JavaScript engines all use this design, alongside the JIT compilers in the same runtimes. V8's team describes theirs in Trash talk: the Orinoco garbage collector.
Why GC pauses happen
If the program keeps changing references while the collector is tracing them, the collector could miss a live object and free it. The simplest fix is to stop the world: pause all application threads during collection.
For a small heap, that pause is a millisecond or less. For a multi-gigabyte heap with a basic collector, it can be much longer, which shows up as a stutter in a game or a latency spike in a server.
Modern collectors shrink pauses in several ways:
- Incremental. Do the work in small slices interleaved with the program.
- Concurrent. Do most of the tracing on background threads while the program runs. Small bookkeeping hooks called write barriers tell the collector about references that change in the meantime.
- Parallel. Use several threads to finish each pause sooner.
Go's collector is concurrent and tuned for short pauses; the Go GC guide explains its trade-offs. Java offers several collectors, including low-pause ones such as ZGC and Shenandoah. The trade is usually lower pause times in exchange for somewhat more CPU and memory.
You can still leak memory
A garbage collector frees unreachable objects. If you keep a reference to something you no longer need, it stays alive. Common causes:
- A cache or map that only ever grows.
- Event listeners or callbacks that are never removed.
- Long-lived collections that hold on to finished work.
- Closures that capture large objects.
Tools such as heap snapshots in browser developer tools, or heap profilers in Java and Go, show what is holding memory.
Reducing GC pressure
- Allocate less in hot paths. Reuse buffers; avoid creating objects in tight loops.
- Keep short-lived objects short-lived. They are cheap to collect in the young generation.
- Size the heap sensibly. Too small means constant collections; too large can mean long ones and may push the machine into swapping.
- Pick the right collector where your runtime offers a choice.
- Measure first. Enable GC logs before changing anything.
The third way: ownership
Rust avoids both manual freeing and a garbage collector. Its compiler tracks which variable owns each value and inserts the free automatically when the owner goes out of scope. This gives predictable performance, at the cost of stricter rules for the programmer.
Frequently asked questions
Does garbage collection make programs slow?
It adds overhead, but allocation in a GC runtime is often very fast, and for most applications the cost is modest. It matters most for low-latency systems.
What is a stop-the-world pause?
A moment when the runtime halts all application threads so the collector can work safely.
Can I force a garbage collection?
Most runtimes offer a call such as System.gc() or gc.collect(), but it is rarely a good idea. The runtime usually knows better when to collect.
Why does my app's memory not drop after a collection?
Runtimes often keep freed memory for reuse instead of returning it to the operating system straight away.
Conclusion
Garbage collection trades a little throughput and predictability for freedom from a whole family of memory bugs. Tracing finds what is reachable, generations make the common case cheap, and concurrent techniques keep pauses short. Knowing how your runtime's collector works turns mysterious pauses and growing memory into problems you can diagnose.
Related articles
- Stack vs Heap: Where Your Variables Actually Live
- Compiled vs Interpreted vs JIT: Why Languages Run Differently
- Why Your Computer Slows Down When RAM Fills Up
- Why Immutability Makes Code Easier to Reason About
