Garbage Collection

Understand how the JVM finds and frees unused objects, which collector to pick for a service, and the habits that keep pauses short.

Syntaxjava -XX:+UseG1GC -Xmx1g -Xlog:gc -jar app.jar java -XX:+UseZGC -Xmx8g -jar app.jar

The garbage collector (GC) frees heap memory that the program can no longer reach. It starts from the roots: local variables in every thread's stack, static fields and a few JVM internals. Anything it can reach by following references from a root is live; everything else is garbage, however many objects point at each other.

Generations

Most objects die young. A request's DTOs, strings and lists are garbage a few milliseconds after the response is sent. Collectors exploit this by putting new objects in a young generation that is collected often and cheaply, and promoting the survivors to an old generation that is collected rarely. Short-lived allocation is cheap in Java; objects that live for a medium time, such as sessions or cache entries that expire after a few minutes, survive long enough to be copied and promoted, and that is what costs GC work.

The collectors in Java 25

CollectorFlagGood for
G1-XX:+UseG1GCThe default. Balanced throughput and pause times for most services.
ZGC-XX:+UseZGCLarge heaps and latency-sensitive APIs. Pauses stay around a millisecond or less. Generational only since Java 24.
Parallel-XX:+UseParallelGCBatch jobs where total throughput matters more than pauses.
Serial-XX:+UseSerialGCSmall heaps and single-CPU containers.

Watch out for the automatic choice: when the JVM sees fewer than two CPUs or less than about 1792 MB of memory, it picks Serial instead of G1. A container limited to one CPU and 1 GB therefore gets Serial unless you set a collector explicitly.

Seeing what it does

Turn on GC logging in every environment; it is cheap and answers most questions after an incident:

java -Xlog:gc:file=gc.log -Xmx512m -jar app.jar

Look at pause times, how often collections run and whether the heap after each full collection keeps rising. A rising floor means something is holding on to objects: a leak, not a tuning problem.

Habits that matter

  • Do not call System.gc() in service code. It is only a request, and when honoured it usually triggers a full collection.
  • Do not use the GC to release files, sockets or connections. Close them with try-with-resources. Finalizers are deprecated for removal; Cleaner exists only as a safety net.
  • Tune the heap size first. Most GC trouble is a heap that is too small for the live data, or a leak. Change collector flags only with GC logs in hand.
  • Weak references (WeakReference, WeakHashMap) let an entry go when nothing else uses its key, but they are no substitute for a cache with a size limit.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Switch a latency-sensitive pricing API with a 16 GB heap to ZGC after GC logs show G1 pauses breaking the response-time target.
  • Set -XX:+UseG1GC explicitly in a one-CPU container so the service does not silently fall back to the Serial collector.
  • Keep GC logs from production pods so a slowly rising heap after each full collection can be traced to a leaking cache.

More examples

A weak reference lets go

Once no strong reference remains, the collector may clear weak references to the object at its next collection. Real code must expect get() to return null at any time and never depends on when that happens.

Example · java
import java.lang.ref.WeakReference;

void main() {
    byte[] report = new byte[1_000_000];
    WeakReference<byte[]> weak = new WeakReference<>(report);
    IO.println("before: " + (weak.get() != null));

    report = null;  // drop the only strong reference
    System.gc();    // only for this demo: a request, not a command
    IO.println("after: " + (weak.get() != null));
}
// Output:
// before: true
// after: false

An oversized allocation fails on its own

The heap had no room for the array, so the allocation failed and nothing else was affected. Catching OutOfMemoryError is only reasonable in a case like this; when the heap fills up gradually, exit and restart instead.

Example · java
void main() {
    try {
        // 12 GB in one array, as a naive 'read the whole upload' might ask for
        long[] huge = new long[1_500_000_000];
        IO.println(huge.length);
    } catch (OutOfMemoryError e) {
        IO.println("OutOfMemoryError: " + e.getMessage());
    }
    int[] small = new int[1_000];
    IO.println("still running, allocated " + small.length + " ints");
}
// Output:
// OutOfMemoryError: Java heap space
// still running, allocated 1000 ints

Release resources explicitly, with Cleaner as a backstop

try-with-resources releases the connection at a known moment. The Cleaner would run the same action only if someone forgot to close it and the object was later collected, which may be never.

Example · java
import java.lang.ref.Cleaner;

static final Cleaner CLEANER = Cleaner.create();

static final class Connection implements AutoCloseable {
    private final Cleaner.Cleanable cleanable;

    Connection(String name) {
        // The action must not capture 'this', or the object never becomes unreachable.
        cleanable = CLEANER.register(this, () -> IO.println("released " + name));
    }

    @Override
    public void close() {
        cleanable.clean(); // runs the action at most once
    }
}

void main() {
    try (Connection c = new Connection("connection-1")) {
        IO.println("querying");
    }
    IO.println("done");
}
// Output:
// querying
// released connection-1
// done

Discussion

  • Be the first to comment on this lesson.