A Java, Go or Python program does not free memory manually: it trusts a garbage collector (GC). Its job is to find the objects that are no longer in use and reclaim their space. The hard part is not finding them, but doing so without stopping the program for too long. To achieve that, modern collectors split memory into generations. Here is the why, and the how.
The problem: memory needs constant “policing”
In C, the programmer reserves memory with malloc() and frees it with free(). It is fast and predictable, but easy to get wrong: you forget to free (memory leak) or you free something still in use (dangling pointer). Languages with a collector remove that decision, but in exchange they need to figure out, at every moment, which memory is alive and which is not. That “figuring out” is the invisible cost the runtime pays.
What is a “live” object and what is “garbage”
The rule is reachability: an object is alive if it can be reached starting from a set of roots. Roots are the program’s static entry points: local variables of running threads, static variables, and references on the call stack. If an object is unreachable from any root, no code can ever use it again, so it is garbage.
To check this, the collector iterates over the object graph following references, like a path finder: it marks every node it touches and, at the end, everything unmarked is discardable. This is called tracing, and it is the basis of all modern GCs.
Mark, sweep and compact
The simplest algorithm is mark-sweep: first mark what is reachable, then sweep the memory freeing what is unmarked. It has two problems: the space becomes fragmented (holes between useful objects) and future allocation turns slow. That is why a third phase is added, mark-compact: after marking, the live objects are moved together to one end, leaving a contiguous free block. The catch is that moving objects forces rewriting every reference, which is expensive.
The observation that changes everything: objects die young
When measuring real programs, an empirical pattern emerged called the weak generational hypothesis: the vast majority of objects are created and stop being useful within the first instants of their life (for example, a temporary iterator or a buffer for an HTTP request). Very few survive for long. That suggests a brilliant idea: why trace ALL of memory if almost everything discarded is young?
Splitting memory: the generational solution
Instead of a single heap, generations are used. The HotSpot JVM collector is the classic example: the young generation is split into an Eden area and two Survivor spaces (S0 and S1). New objects are created in Eden. When Eden fills up, a minor GC runs that traces only the young generation; the objects that survive are copied to a Survivor space and, after several rounds without dying, are promoted to the old generation.
The advantage is huge: since almost everything dies young, minor collections are frequent but cheap — they only touch a small fraction of the heap. Objects that have been alive for a long time go to the old generation, which is collected far less often through a major collection.
Stop-the-world and pauses
To safely copy or move objects, the collector needs the program’s threads to stop at a safe point; this is called stop-the-world. Minor collections took milliseconds; major collections of the old generation could take much longer, and that showed up as “freezes” in applications.
That is the recent history of memory management: reducing those pauses. Modern collectors such as G1 (HotSpot) split the heap into regions and collect in parts, and ZGC or Shenandoah do almost all the work in parallel with the application threads, but always on the same generational foundation.
A counterpoint: Go and its concurrent GC
Go is an interesting case to compare. Its collector is non-generational: it does not split the heap by age, it uses a single generation and a concurrent tracing variant in which collection happens while the program runs, with very small and bounded pauses. It is the counterpart: fewer data structures to maintain in exchange for paying the cost of concurrency in every cycle. The choice is not “the best GC”, but the one that fits the pause requirements of the language.
Conclusion
Behind every application that “manages its own memory” there is a delicate balance between allocation speed and pause duration. The generational idea — separating what dies young from what lives long — remains the dominant strategy because it exploits a real statistical property of programs. The next time your profiler shows a slow GC stall, you now know what is really happening: memory is catching up with the garbage you left behind.





