When many developers first hear about Java's Garbage Collector, they often associate it with one thing:
"Automatic memory management comes at the cost of performance."
This perception made sense twenty years ago.
Today, however, some of the largest systems in the world run on the JVM, handling millions of requests per second with strict latency requirements.
Companies like Netflix, Uber, LinkedIn, and many banks rely heavily on Java in production environments where performance is not optional.
So what changed?
The answer lies in understanding how the Java Garbage Collector actually works.
The Memory Problem Every Language Must Solve
Imagine the following code:
User user = new User();
The application allocates memory for the User object.
Eventually, that object is no longer needed.
The question is simple:
Who is responsible for freeing that memory?
In languages such as C, the developer is responsible:
User* user = malloc(sizeof(User));
free(user);
Forgetting to free memory causes leaks.
Freeing memory too early can lead to crashes and undefined behavior.
Java chose a different approach:
The JVM itself determines when an object is no longer reachable and automatically reclaims its memory.
This process is known as Garbage Collection.
How Does Java Know an Object Is Dead?
Java does not determine whether an object is alive based on variables or scope.
Instead, it uses a concept called reachability.
Consider:
User user = new User();
The variable user references the object.
As long as that reference exists, the object is considered alive.
Now:
user = null;
If no other references point to that object, it becomes unreachable.
Unreachable objects become candidates for garbage collection.
This approach is significantly safer than manual memory management because the JVM avoids both accidental deallocation and most memory corruption issues.
The Observation That Changed Everything
The JVM is built around an observation known as the Generational Hypothesis:
Most objects die young.
Surprisingly, this is true for the majority of modern applications.
Examples include:
- HTTP request objects
- DTOs
- JSON parsing structures
- Temporary strings
- Stream operations
- Intermediate collections
These objects often live for only a few milliseconds.
Meanwhile, objects that survive for longer periods tend to remain alive for a very long time:
- Caches
- Singleton services
- Application configurations
- Database connection pools
This observation fundamentally shaped the design of Java's Garbage Collectors.
The Heap Is Divided Into Generations
Instead of treating all objects equally, Java separates memory into generations.
Heap
│
├── Young Generation
│ ├── Eden
│ ├── Survivor 0
│ └── Survivor 1
│
└── Old Generation
Eden: Where Objects Are Born
Almost every object starts its life in Eden Space.
new User();
new Order();
new Product();
All of them are allocated inside Eden.
When Eden becomes full, the JVM triggers a Minor GC.
Minor GC
During a Minor GC, the JVM identifies which objects in Eden are still alive.
Dead objects are discarded immediately.
Live objects are copied to one of the Survivor spaces.
Eden → Survivor
This process is extremely efficient because most objects are already dead.
Remember the Generational Hypothesis:
Most objects die young.
As a result, the JVM often removes the vast majority of objects during every Minor GC cycle.
Survivor Spaces
The JVM alternates between two survivor areas:
- Survivor 0
- Survivor 1
Objects bounce between these spaces while they survive multiple garbage collection cycles.
Each time an object survives, its age increases.
Age 1
Age 2
Age 3
...
Eventually, the JVM decides that the object is probably long-lived.
At this point, it gets promoted.
Promotion to Old Generation
Objects that survive enough collection cycles are moved to the Old Generation.
Young Generation
↓
Old Generation
Typical examples include:
- Spring Beans
- Caches
- Singleton instances
- Application configuration objects
These objects are expected to remain alive for much longer periods.
Major GC and Full GC
Cleaning the Old Generation is considerably more expensive.
This process is often called a Major GC.
In some situations, the JVM may need to collect the entire heap:
- Young Generation
- Old Generation
- Metaspace
This is known as a Full GC.
Historically, Full GCs were among the most feared events in Java applications because they frequently paused the application for long periods.
Modern collectors significantly reduced this problem.
The Stop-The-World Moment
During certain phases of garbage collection, the JVM pauses all application threads.
This event is called:
Stop-The-World (STW)
While the JVM performs critical operations, application threads remain suspended.
The goal of modern collectors is not to eliminate these pauses entirely, but rather to make them extremely short and predictable.
Enter G1GC
Since Java 9, the default collector is Garbage First (G1).
Instead of dividing memory into large contiguous spaces, G1 divides the heap into many small regions.
+----+----+----+----+
| R1 | R2 | R3 | R4 |
+----+----+----+----+
| R5 | R6 | R7 | R8 |
+----+----+----+----+
Some regions contain young objects.
Others contain old objects.
This design allows G1 to focus on collecting the regions that provide the highest memory recovery with the smallest pause times.
Hence the name:
Garbage First.
The Secret Behind Fast Object Allocation
One of the biggest misconceptions in Java is:
Creating objects is expensive.
In reality, object allocation in Java is often incredibly cheap.
Most allocations happen using a technique called Bump Pointer Allocation.
| Object A | Object B | Free Space |
^
Pointer
Creating a new object frequently means little more than:
pointer += object_size
No complex memory search.
No traversal.
No expensive allocation algorithms.
Just moving a pointer forward.
Thread Local Allocation Buffers (TLAB)
The JVM goes even further.
Each thread often receives its own allocation area called a Thread Local Allocation Buffer (TLAB).
Instead of multiple threads competing for access to the heap, they allocate objects inside their own local buffers.
This dramatically reduces contention and synchronization overhead.
For most applications, object allocation becomes almost lock-free.
Sometimes the Object Never Exists
Modern JVMs perform sophisticated optimizations.
One of the most impressive is Escape Analysis.
public int sum() {
Point point = new Point(10, 20);
return point.x + point.y;
}
If the JVM determines that point never escapes the method, it may avoid allocating the object entirely.
In some cases:
The object is never created.
Is Garbage Collection Free?
No.
Garbage Collection introduces overhead.
The JVM spends CPU cycles:
- Identifying live objects
- Moving objects
- Compacting memory
- Updating references
However, the trade-off has proven extremely successful.
Developers gain:
- Memory safety
- Higher productivity
- Fewer crashes caused by invalid pointers
- Simpler application code
Final Thoughts
The Garbage Collector is often described as a convenience feature.
In reality, it is much more than that.
The JVM combines decades of research in:
- Allocation algorithms
- Concurrent programming
- Memory compaction
- Cache locality
- Latency reduction
Modern Java applications are not fast despite the Garbage Collector.
In many cases, they are fast precisely because of the engineering behind it.
The next time you write:
new User();
remember that behind this single line, the JVM is leveraging decades of computer science research to make that operation as efficient as possible.
References
- Oracle Java HotSpot Garbage Collection Tuning Guide
- Oracle G1 Garbage Collector Documentation
- Aleksey Shipilev — JVM Anatomy Quarks
- Scott Oaks — Java Performance: The Definitive Guide
- Plumbr Garbage Collection Handbook
- OpenJDK Documentation
- GCeasy JVM Performance Articles
Top comments (0)