Q3 of 40 · Core Java

How does garbage collection work in modern JVMs?

Core JavaSeniorjava-jvmgarbage-collectionmemoryg1gcperformance

Short answer

Short answer: Modern JVM GC is generational: short-lived objects are collected cheaply in the Young generation (Eden + Survivor spaces); long-lived objects are promoted to Old gen and collected less frequently. G1GC (default from Java 9) targets configurable pause goals by running concurrent marking phases alongside application threads.

Detail

Java's garbage collector tracks object reachability from root references (stack frames, static fields, JNI handles). Any object not reachable from a root is eligible for collection — you don't manage memory manually.

Generational hypothesis: most objects die young. The heap is split into:

  • Young generation — Eden + two Survivor spaces (S0/S1). New objects allocate in Eden. A minor GC copies live objects to a Survivor space and discards the dead. Objects that survive several collections are promoted to Old gen. Minor GCs are stop-the-world but brief because Young gen is small.
  • Old generation — long-lived objects. Major GC here is more expensive and was historically the pause culprit.
  • Metaspace (Java 8+) — class metadata, replaces the old PermGen.

G1GC (Garbage First, default from Java 9) divides the heap into equal-sized regions (1–32 MB) and prioritises collecting regions with the most garbage first. It runs concurrent marking phases alongside the application to minimise stop-the-world pauses, targeting a configurable pause goal (-XX:MaxGCPauseMillis, default 200ms).

ZGC and Shenandoah go further — most work happens concurrently, with pauses typically under 1ms even on multi-TB heaps, using coloured pointers or read/write barriers to update object references on the fly as objects are moved.

For QA automation, GC rarely matters at unit/integration scale. It becomes relevant in performance tests (GC pauses inflate latency percentiles), large parallel Selenium/Playwright runs under heap pressure, or when writing custom JMeter plugins. Knowing the basics helps you interpret JVM flags in CI config and diagnose test flakiness caused by GC pauses in latency-sensitive environments.

// EXAMPLE

JvmFlags.java

// JVM flags for CI test runs — tune GC for parallel test suites
// -XX:+UseG1GC                  (default Java 9+, explicit is fine)
// -XX:MaxGCPauseMillis=200       pause target in ms
// -Xms512m -Xmx2g               fix initial/max heap to avoid resize GCs
// -Xlog:gc*:file=gc.log:time     write GC log for post-run analysis

// Avoiding GC pressure in test data builders
public static List<User> buildUsers(int count) {
    // Pre-size to avoid multiple ArrayList resizes
    List<User> users = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
        // Don't retain references beyond this scope
        users.add(new User("user" + i, "user" + i + "@example.com"));
    }
    return users; // caller controls lifetime, no static refs
}

// Memory leak pattern to avoid in long-running test suites
private static final List<byte[]> LEAK = new ArrayList<>();
// Adding to a static list without clearing it keeps objects alive
// across test classes — causes OOM in large suites

// WHAT INTERVIEWERS LOOK FOR

Generational collection model (Eden/Survivor/Old), stop-the-world tradeoff, G1GC as the modern default, and awareness of ultra-low-pause alternatives (ZGC/Shenandoah). Bonus: practical relevance to performance testing or CI environments, and awareness of static-collection memory leaks in test suites.

// COMMON PITFALL

Saying 'Java has a garbage collector so you never have memory issues.' Memory leaks are possible if you hold references longer than needed — a growing static collection in a long-running test suite is the classic example. GC pauses are also real latency events in performance tests.