2 min read
Memory in the JVM· Part 1 of 1How the garbage collector really works
Generations, pauses and modern collectors: what the JVM does with memory you no longer use, and how to tell whether GC is your problem.
Hi everyone. Today we talk about something we use every day without thinking about it: the garbage collector. Almost everyone knows it “frees memory”, but few know what really happens when your application freezes for half a second for no apparent reason.
The core idea: most objects die young
The JVM’s garbage collector relies on an empirical observation called the generational hypothesis: most objects are used for a very short time and then thrown away. Think of a string created to format a log line, or a helper object inside a loop.
That is why memory is split into two main areas:
- Young generation: where every object is born. It is cleaned often and it is very fast, because almost everything in there is already garbage.
- Old generation: where objects that survived several cleanups end up. It is cleaned rarely, but when it happens it costs more.
What “stop the world” means
To move and count objects safely, the collector sometimes has to pause all application threads. This is the famous stop-the-world pause. Nobody notices a few milliseconds; a two-second pause is very noticeable, especially in a service answering real-time requests.
Which collector to pick
Since Java 9 the default collector is G1, designed to balance throughput and latency on medium to large heaps. If latency is critical there are collectors like ZGC, which does almost all of its work concurrently and keeps pauses under a millisecond even with huge heaps.
Before switching collector, though, measure. You can enable GC logging with a single flag:
java -Xlog:gc*:file=gc.log -jar app.jar
If the log shows long, frequent pauses, or an old generation that keeps growing and never shrinks, the problem is real. Often the cause is not the collector but a memory leak in your code: objects you keep referenced by mistake, like a cache that never evicts anything.
// A cache that grows forever: every new key stays in memory for as long as the application runs
private static final Map<String, Report> CACHE = new HashMap<>();
Report load(String id) {
return CACHE.computeIfAbsent(id, this::buildReport);
}
In short
Don’t tune blindly: turn on the log, read the data, and only then decide whether to change collector or fix the code. In the next video we look at how to analyze a heap dump.
