← Back to the section

In languages like C, you have to free memory manually: forget to call free and you get a leak, free it twice and you get a crash. In Java this is handled by the garbage collector (GC): it finds objects that no one needs anymore and reclaims their memory on its own. Let's look at how this works and which settings let you influence it.

Why automatic garbage collection is needed

When you write new Order(), the object is created in a memory area called the heap. As long as there is at least one live reference to the object, it is needed. Once no references remain (for example, the variable went out of scope), the object becomes garbage — it can no longer be reached from running code.

The garbage collector periodically walks through live objects, starting from the "roots" (local variables on the stack, static fields), and everything it cannot reach is considered garbage and freed.

A short rule of thumb: an object is alive if there is a path of references to it from the roots; everything else is garbage.

What this gives you in practice:

  • no need to free memory manually — an entire class of errors disappears (leaks, double frees, access to freed memory);
  • you can focus on the logic instead of tracking who deletes an object and when.

The price is that the moment of collection is chosen not by the programmer but by the runtime, and sometimes collection pauses the application. More on that below.

The heap and generations

Most objects live very briefly: they are created inside a method, do their job and become garbage. The generational model of the heap is built on this observation. The heap is divided into two main parts:

  • Young Generation — all new objects land here. It fills up quickly.
  • Old Generation — objects that survived several collections in the young generation "move" here, that is, those that live a long time.

Correspondingly, there are two kinds of collections:

  • minor GC — collection only in the young generation. It happens often and runs quickly, because most young objects are already garbage and there are few live ones to check.
  • major GC (or full GC) — affects the old generation. It happens less often but takes longer.

Stop-the-world pauses

To safely recount references and move objects, the collector sometimes needs to briefly stop all application code. This is the stop-the-world pause: the application freezes and responds to nothing while the GC does its work, then continues.

In plain terms: imagine a librarian tidying up the shelves. While readers are walking around and rearranging books, nothing can be counted precisely. So the entrance is closed for a short time, order is restored, and it opens again. The more books (objects) there are and the longer the cleanup, the more noticeable the delay is for readers.

The whole struggle between different garbage collectors comes down to making such pauses as short and rare as possible without sacrificing too much overall performance.

The main collectors and when to use each

Java 21 ships several collectors in a single JVM (HotSpot). The choice is always a trade-off between two quantities:

  • throughput — what fraction of time the machine spends on useful work rather than on collection;
  • latency — how short the stop-the-world pauses are.
CollectorStrengthWhen it fits
Serialminimal overheadsmall applications, little memory, a single core
Parallelmaximum throughputbatch processing where pauses are not critical
G1balance of pauses and throughputthe default, suits most services
ZGC / Shenandoahvery short pauseslarge heaps, requirements for consistently low latency

Serial GC is the simplest. It does all the work in a single thread and always with a stop-the-world pause. It is good where there is not much data and extra threads and memory would be wasteful.

Parallel GC (also called the throughput collector) collects garbage using several threads. There are pauses, but over the total runtime it spends the least on collection — that is, it gives the maximum CPU time to the application itself. It suits tasks where overall processing speed matters and short stalls are tolerable.

G1 GC (Garbage-First) is the default collector since Java 9. It divides the heap into many small regions and collects first those with the most garbage (hence the name). It tries to keep pauses within a given budget. This is a reasonable balance that suits most server applications with no tuning at all.

ZGC and Shenandoah are collectors focused on minimal pauses. They do most of their work concurrently with the application, barely stopping it, so pauses stay very short even on heaps of tens and hundreds of gigabytes. The price is slightly higher CPU and memory usage. They are needed where noticeable stalls are unacceptable (for example, a service with strict response-time requirements).

A short rule of thumb: by default — G1; need maximum throughput — Parallel; need consistently tiny pauses on a large heap — ZGC.

Key startup parameters

The collector's behavior and the heap size are set by flags when launching java.

Heap size:

# initial heap size 512 MB, maximum 2 GB
java -Xms512m -Xmx2g -jar app.jar
  • -Xms — the initial heap size;
  • -Xmx — the maximum heap size.

A common trick for servers is to set -Xms equal to -Xmx. Then the JVM reserves the entire heap right away and does not spend time gradually expanding it under load.

Choosing a collector:

java -XX:+UseG1GC   -jar app.jar   # G1 (the default anyway)
java -XX:+UseZGC    -jar app.jar   # ZGC, short pauses
java -XX:+UseParallelGC -jar app.jar   # Parallel, maximum throughput

A pause-time goal hint (for G1):

# ask G1 to try to keep pauses around 100 ms
java -XX:MaxGCPauseMillis=100 -jar app.jar

-XX:MaxGCPauseMillis is a goal, not a guarantee. The JVM will try to honor it by balancing region sizes and collection frequency, but it cannot promise an exact value.

Garbage collection logs:

# write GC events to the console
java -Xlog:gc -jar app.jar

# more detail and write to a file
java -Xlog:gc*:file=gc.log:time,uptime -jar app.jar

-Xlog:gc enables the collection event log: you can see when and which collector ran, how long the pause was, and how much memory was freed. This is the first thing to look at if you suspect the application is stalling because of GC.

How to choose and not overdo it

The main advice is to start with the default settings. G1 in Java 21 suits most applications well, and tuning hurts more often than it helps: a flag tweaked "by eye" easily makes things worse.

A sensible order of actions:

  1. Run the application as is (G1 by default).
  2. Set -Xmx to match the actual available memory — this is the most influential parameter.
  3. If there is a performance problem, measure first: enable -Xlog:gc and check whether the issue really is garbage collection rather than your code or the database.
  4. Only if pauses genuinely get in the way should you try a different collector (ZGC for short pauses) or the -XX:MaxGCPauseMillis goal, one change at a time and with before/after measurements.

A short rule of thumb: don't tune the GC until the logs prove it is the problem.

In short

  • The garbage collector frees the memory of objects that no longer have a path of references from the roots on its own — no manual free is needed.
  • The heap is divided into young and old generations; minor GC cleans the young one (often and quickly), major GC cleans the old one (rarely and slowly).
  • A stop-the-world pause is a brief stop of the application during collection; the collectors' job is to make pauses shorter and rarer.
  • Serial — for small applications, Parallel — for maximum throughput, G1 — the balance and the default, ZGC and Shenandoah — tiny pauses on large heaps.
  • -Xms/-Xmx set the initial and maximum heap size; -XX:+UseG1GC/-XX:+UseZGC choose the collector; -XX:MaxGCPauseMillis is the pause goal; -Xlog:gc enables logs.
  • Start with the default settings and don't tune the GC until the logs prove the bottleneck is really there.
  • Java developer tools — how to run, build and profile an application.
  • Collections — where the objects that GC later collects live.
  • Records and modern syntax — compact objects and modern language features.