RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
MediumJava#118 min readJul 11, 2026

Explain Garbage Collection.

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Garbage Collection is Java's built-in cleanup crew — interviewers love it because it reveals whether you understand memory, pauses, and leaks, not just syntax.

Question: Explain Garbage Collection.

Answer: Garbage Collection, or GC, is the JVM's automatic process for finding objects on the heap that your program can no longer reach and reclaiming their memory. An object becomes eligible for GC when no live reference can get to it from a GC root, such as a local variable, static field, or active thread. This helps Java avoid many manual memory bugs, but it does not mean memory is released at a predictable instant.

Interview-Ready Answer: In Java, Garbage Collection is the JVM's automatic memory management for heap objects. I think of it as the JVM tracing which objects are still reachable from GC roots like stack variables and static fields; anything unreachable becomes eligible for reclamation. The important interview point is that GC is automatic but nondeterministic — for example, System.gc() is only a hint, and the collector choice, such as G1, affects pause times and throughput.

🧠 Memory Map
Memory map — visual summary of this topic

Detailed Explanation:

What GC is actually doing

Java objects live on the heap, which is memory managed by the JVM. GC is a tracing collector: it starts from known live entry points, called GC roots (for example, thread stacks, static fields, and JNI references), and follows references to mark everything that is still reachable. Anything not marked is considered garbage.

  1. Allocate quickly: The JVM usually gives new objects memory from a young area of the heap. A common optimization is a TLAB (thread-local allocation buffer), which is a small private chunk for each thread, so object allocation is often close to constant-time and very fast.
  2. Find the roots: The collector pauses or briefly coordinates with application threads so it can safely inspect roots. This pause is called stop-the-world (STW), meaning your program threads are temporarily paused while some GC work happens.
  3. Mark live objects: Starting from those roots, GC walks object graphs and marks every object that can still be reached. This is why cycles are fine in Java: if an object graph is unreachable, it can still be collected even if the objects point to each other.
  4. Collect garbage: Unreachable objects are then swept away or their memory is reclaimed by copying live objects elsewhere. Copying collectors also compact memory, which reduces fragmentation (free space split into tiny pieces).
  5. Promote survivors: Objects that survive several young collections are often moved to the old generation because they are likely to live longer. This is based on the generational hypothesis: most objects die young.
  6. Repeat as needed: The JVM keeps doing this as the program allocates more objects. GC is automatic, but it is not magic — it trades CPU time and occasional pauses for safety and simpler coding.

Young vs old generation

Most HotSpot collectors use a generational layout. The young generation holds newly created objects and is collected often; the old generation holds long-lived objects and is collected less frequently. That design is practical because many temporary objects die within milliseconds, so reclaiming them early is cheaper than scanning a huge heap every time.

Common collectors and trade-offs

CollectorGoalPause styleGood for
SerialSimpleSingle-threaded STWSmall heaps
ParallelThroughputMulti-threaded STWBatch jobs
G1BalancedMostly brief STWGeneral servers
ZGCLow pauseMostly concurrentLarge heaps

In modern HotSpot, G1 is the default general-purpose collector for most server-style workloads since JDK 9. Older JDK 8 server configurations commonly used Parallel GC. Low-pause collectors like ZGC aim for very short pauses, but they usually spend more CPU coordinating work concurrently.

Performance and edge cases

  • No fixed Big-O: GC cost depends on the live set — the objects still reachable — not just total heap size. In practice, allocation is usually amortized O(1), while collection work grows with the amount of live data that must be scanned or copied.
  • Real numbers: With a healthy heap, pauses may be tiny; with a large live set or old-gen pressure, pauses can become tens or hundreds of milliseconds, and sometimes longer. Low-latency collectors can reduce pauses dramatically, but not make them disappear.
  • Heap sizing matters: -Xms sets initial heap size and -Xmx sets maximum heap size. The JVM uses ergonomics to choose defaults from available memory, and modern versions respect container limits.
  • GC does not manage everything: It only reclaims heap objects. Native memory, direct buffers, and file handles need separate cleanup patterns; GC will not reliably close them for you.
  • Memory leaks still happen in Java: If a static cache, singleton, or long-lived collection keeps references alive, GC cannot collect them. So Java removes manual free calls, but it does not remove the need to design object lifetimes carefully.

Memory model shortcut: think reachable = alive, unreachable = garbage. That is the core rule interviewers want you to say clearly.

Real-World Example: Imagine an e-commerce checkout service on Black Friday. Every request creates cart objects, discount calculations, and payment data. A developer adds a static map as a quick cache and forgets to evict old entries, so finished carts stay strongly reachable forever.

  1. Traffic ramps up and the service allocates many short-lived objects for each checkout request.
  2. The young generation fills quickly, so GC runs more often to clean temporary request data.
  3. Because the static cache keeps old carts alive, the old generation keeps growing too.
  4. Eventually the JVM spends more time collecting than serving requests, and pause times increase.
  5. Users see slow page loads, checkout timeouts, and retry storms; logs may show GC overhead limit exceeded or repeated allocation-failure pauses.

The lesson is important: a GC problem is often really a reference-retention bug. The JVM is doing the right thing; the application is accidentally holding on to objects that should have become unreachable. In production, that difference decides whether you fix the heap size, the collector, or the cache design.

Java
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class GarbageCollectionDemo {

    // A static list is a classic source of accidental memory retention:
    // as long as this list holds an object, the GC must treat it as reachable.
    private static final List<Payload> STATIC_CACHE = new ArrayList<>();

    private static final class Payload {
        private final String name;
        private final byte[] data;

        private Payload(String name, int kilobytes) {
            this.name = name;
            this.data = new byte[kilobytes * 1024];
        }

        @Override
        public String toString() {
            return name + " (" + data.length + " bytes)";
        }
    }

    public static void main(String[] args) throws Exception {
        demonstrateCollectibleObject();
        System.out.println();
        demonstrateRetainedObject();
    }

    private static void demonstrateCollectibleObject() throws InterruptedException {
        System.out.println("=== Collectible object ===");
        ReferenceQueue<Payload> queue = new ReferenceQueue<>();
        Payload payload = new Payload("temporary", 256);
        WeakReference<Payload> weak = new WeakReference<>(payload, queue);

        // Remove the strong reference; now only the weak reference remains.
        // WeakReference does not keep the object alive.
        payload = null;

        forceGc();
        report(weak, queue, "temporary");
    }

    private static void demonstrateRetainedObject() throws InterruptedException {
        System.out.println("=== Retained by a strong reference ===");
        ReferenceQueue<Payload> queue = new ReferenceQueue<>();
        Payload payload = new Payload("cached", 256);
        WeakReference<Payload> weak = new WeakReference<>(payload, queue);

        // This strong reference prevents collection.
        // GC cannot reclaim a live object, even if it is only held by a cache.
        STATIC_CACHE.add(payload);
        payload = null;

        forceGc();
        report(weak, queue, "cached");

        // Cleanup so the demo does not keep growing if run repeatedly.
        STATIC_CACHE.clear();
    }

    private static void forceGc() throws InterruptedException {
        // System.gc() is only a hint, not a command.
        // We retry a few times and add small allocation pressure to make collection more likely.
        for (int i = 0; i < 5; i++) {
            System.gc();
            byte[] pressure = new byte[1024 * 1024];
            pressure[0] = 1;
            Thread.sleep(100);
        }
    }

    private static void report(WeakReference<Payload> weak, ReferenceQueue<Payload> queue, String label) {
        Payload current = weak.get();
        if (current == null) {
            System.out.println(label + ": collected");
        } else {
            System.out.println(label + ": still reachable -> " + current);
        }

        System.out.println("ReferenceQueue enqueued? " + (queue.poll() != null));
        System.out.println("Key idea: only unreachable objects are eligible for GC; strong references keep them alive.");
    }
}

Follow-up & Tricky Questions:

  • What is a GC root? A GC root is a starting point the collector treats as live, such as thread stacks, static fields, and JNI references. If an object can be reached from a GC root, it cannot be reclaimed.
  • What is the difference between minor GC and full GC? Minor GC usually means collecting the young generation, which is cheaper because most objects die there. Full GC is broader and may involve the old generation and more heap regions, so it is usually more expensive and causes longer pauses.
  • Why is G1 often preferred for server apps? G1 tries to balance throughput and latency by collecting regions incrementally instead of doing one huge stop-the-world sweep. It is a practical default when you want good average performance without tuning for a very narrow workload.
  • What does stop-the-world mean? It means application threads are paused while the JVM performs a GC phase that needs exclusive control of the heap. Even concurrent collectors still have some brief STW phases.
  • How do weak references behave? A WeakReference does not keep an object alive, so the GC can clear it as soon as there are no strong references. That is why weak references are useful for caches that should not prevent cleanup.
  • Does GC collect objects with cyclic references? Yes. Java uses tracing GC, so a cycle is collectible if nothing reachable from a GC root points into it. This is a common difference from simple reference-counting systems.
  • Does System.gc() force collection? No; it is only a hint to the JVM. The JVM may ignore it or delay action, so you should never depend on it for correctness or performance.
  • Does GC free native resources like files or sockets? No, not directly. Heap memory may be reclaimed by GC, but file descriptors, sockets, and other native resources should be closed explicitly, usually with try-with-resources.
  • Are finalizers a good cleanup mechanism? No. Finalization is unreliable, slow, and deprecated for removal; use explicit cleanup with AutoCloseable instead.

Common Mistakes:

  • Mistake: Saying GC frees memory immediately. Correction: GC runs when the JVM decides, so reclamation is automatic but nondeterministic.
  • Mistake: Thinking System.gc() guarantees cleanup. Correction: It is only a hint, and the JVM can ignore it.
  • Mistake: Assuming Java cannot have memory leaks. Correction: Leaks still happen when long-lived references keep dead data reachable.
  • Mistake: Treating GC as responsible for files and sockets. Correction: GC manages heap objects; external resources need explicit close calls.

Memory Hook: Think of GC as a librarian who removes books only when no reader can reach them anymore. If a bookmark, hand, or shelf label still points to the book, it stays.

Cheat Sheet:

  • GC = automatic heap cleanup in the JVM.
  • Reachable from GC roots = alive; unreachable = garbage.
  • Young objects die often, so generational GC is faster.
  • G1 is the common default in modern HotSpot.
  • System.gc() is a hint, not a command.
  • GC does not fix leaks caused by strong references or manage native resources.

Practice Tasks:

  • Run the code example and watch how a weakly referenced object can be collected while a cached one stays alive.
  • Add a static List cache to a small program, forget to clear it, and observe rising memory usage.
  • Run any Java app with -Xlog:gc and read the pause messages to connect theory with real JVM behavior.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class GarbageCollectionDemo { // A static list is a classic source of accidental memory retention: // as long as this list holds an object, the GC must treat it as reachable. private static final List<Payload> STATIC_CACHE = new ArrayList<>(); private static final class Payload { private final String name; private final byte[] data; private Payload(String name, int kilobytes) { this.name = name; this.data = new byte[kilobytes * 1024]; } @Override public String toString() { return name + " (" + data.length + " bytes)"; } } public static void main(String[] args) throws Exception { demonstrateCollectibleObject(); System.out.println(); demonstrateRetainedObject(); } private static void demonstrateCollectibleObject() throws InterruptedException { System.out.println("=== Collectible object ==="); ReferenceQueue<Payload> queue = new ReferenceQueue<>(); Payload payload = new Payload("temporary", 256); WeakReference<Payload> weak = new WeakReference<>(payload, queue); // Remove the strong reference; now only the weak reference remains. // WeakReference does not keep the object alive. payload = null; forceGc(); report(weak, queue, "temporary"); } private static void demonstrateRetainedObject() throws InterruptedException { System.out.println("=== Retained by a strong reference ==="); ReferenceQueue<Payload> queue = new ReferenceQueue<>(); Payload payload = new Payload("cached", 256); WeakReference<Payload> weak = new WeakReference<>(payload, queue); // This strong reference prevents collection. // GC cannot reclaim a live object, even if it is only held by a cache. STATIC_CACHE.add(payload); payload = null; forceGc(); report(weak, queue, "cached"); // Cleanup so the demo does not keep growing if run repeatedly. STATIC_CACHE.clear(); } private static void forceGc() throws InterruptedException { // System.gc() is only a hint, not a command. // We retry a few times and add small allocation pressure to make collection more likely. for (int i = 0; i < 5; i++) { System.gc(); byte[] pressure = new byte[1024 * 1024]; pressure[0] = 1; Thread.sleep(100); } } private static void report(WeakReference<Payload> weak, ReferenceQueue<Payload> queue, String label) { Payload current = weak.get(); if (current == null) { System.out.println(label + ": collected"); } else { System.out.println(label + ": still reachable -> " + current); } System.out.println("ReferenceQueue enqueued? " + (queue.poll() != null)); System.out.println("Key idea: only unreachable objects are eligible for GC; strong references keep them alive."); } }