Skip to main content

Java

Java Garbage Collection: A Detailed Guide

·

Diagram illustrating Java garbage collection mechanism and heap memory regions

Java heap memory split into Young Generation, Old Generation, and Metaspace regions

Java's garbage collection (GC) mechanism automatically reclaims heap memory from objects that are no longer reachable, freeing developers from manual malloc/free cycles while keeping applications stable under load. This guide covers the memory model, the 6 major collector types, the mark-and-sweep process, tuning strategies, and how to track down and prevent memory leaks.

If you're working through a Java assignment that touches memory management and need expert help, Java Assignment Help connects you with developers who specialize in the JVM.

Why Memory Management Matters in Java

Java divides program memory into two main regions: the stack and the heap. The stack holds method frames and local variables. The heap is where all dynamically allocated objects live.

Before automatic GC, developers manually allocated and freed memory. That approach created two chronic failure modes: memory leaks (forgetting to free) and dangling pointers (freeing too early). Java's GC eliminates both by tracking object reachability and reclaiming memory on its own schedule.

The tradeoff is that the GC runs on its own schedule, not yours. Understanding how it decides when to collect, and how to tune that decision, is the core skill covered here.

What Java Garbage Collection Actually Does

GC identifies objects that no references point to, then reclaims their heap space. An object becomes eligible for collection the moment no live thread holds a reference to it, directly or through a reference chain.

The JVM tracks reachability from a set of GC roots: active thread stacks, static fields, JNI references, and class loaders. Anything reachable from a GC root is live. Anything not reachable is garbage.

Two consequences follow:

  • Circular references are handled correctly. Two objects pointing at each other with no path from a GC root are both collectable, unlike reference-counting schemes.
  • Objects are not collected the instant they become unreachable. Collection happens in discrete phases, which can introduce pauses.

6 Types of Java Garbage Collectors

The JVM ships 6 GC implementations. Selecting the wrong one for your workload is the most common source of GC-related performance problems.

Serial Garbage Collector runs all GC work on a single thread and stops all application threads during collection. It is suitable for small single-threaded applications or constrained environments where heap size stays under a few hundred megabytes.

Parallel Garbage Collector (the "throughput collector") uses multiple threads for GC, reducing wall-clock pause time on multicore hardware. The JVM uses it by default on server-class machines in Java 8. Best for batch workloads where throughput matters more than individual pause length.

CMS (Concurrent Mark-Sweep) Collector performs most marking concurrently with application threads, keeping pauses short. It was the standard choice for interactive server applications with large heaps before G1 displaced it. CMS is deprecated in Java 9 and removed in Java 14. If you are still on Java 8, G1 is the migration target.

G1 (Garbage-First) Collector divides the heap into equal-sized regions (1 MB to 32 MB each) and collects the regions with the most garbage first. It targets a configurable pause goal (-XX:MaxGCPauseMillis) and balances throughput with low latency. G1 became the default in Java 9. It handles heaps from 4 GB to hundreds of gigabytes.

ZGC performs almost all work concurrently, targeting sub-millisecond pauses regardless of heap size. It became production-ready in Java 15 and scales to terabyte heaps. ZGC is the right choice for latency-sensitive systems: financial tick processing, gaming backends, real-time APIs.

Shenandoah Garbage Collector also targets ultra-low pauses and performs concurrent compaction, which ZGC does not. It is available in OpenJDK builds from Red Hat. Shenandoah works well for applications with unpredictable allocation patterns and strict latency budgets.

| Collector | Default since | Pause model | Heap sweet spot | |---|---|---|---| | Serial | (never default) | Stop-the-world | Small, <200 MB | | Parallel | Java 8 | Stop-the-world | Medium-large | | CMS | (removed Java 14) | Mostly concurrent | Large | | G1 | Java 9 | Concurrent + short STW | 4 GB+ | | ZGC | Java 15 | Sub-millisecond | Very large | | Shenandoah | OpenJDK (Red Hat) | Concurrent compact | Variable |

The Java Memory Model and Heap Regions

The heap is divided into generations based on the Generational Hypothesis: most objects die young. Treating short-lived and long-lived objects separately makes collection far more efficient.

Young Generation holds newly created objects. It is split into 3 areas: Eden space, Survivor space S0, and Survivor space S1. New objects enter Eden. When Eden fills, a Minor GC runs, promoting survivors into one of the Survivor spaces. Objects that survive enough Minor GC cycles are promoted to the Old Generation.

Old Generation (Tenured Generation) holds objects that survived multiple Minor GC cycles. Major GC (or Full GC) runs here less frequently but takes longer because it covers more memory.

Metaspace (Java 8+, replacing the Permanent Generation) stores class metadata, method bytecode, and interned strings. Metaspace grows automatically up to the OS limit unless you cap it with -XX:MaxMetaspaceSize. A runaway class loader that never unloads classes can exhaust Metaspace.

GC Algorithms: Mark, Sweep, and Compact

Three core operations underlie every collector.

Mark traverses the object graph from GC roots, setting a bit on every reachable object. Cost is proportional to the number of live objects.

Sweep scans the heap and reclaims memory from unmarked objects. This produces free blocks of varying sizes, leading to fragmentation over time.

Compact moves live objects together to one end of the heap, eliminating fragmentation and making future allocation cheaper (a pointer bump instead of a free-list search). Compaction requires moving objects and updating all references to them, which is why it triggers stop-the-world pauses.

Minor GC, Major GC, and Full GC

Minor GC collects the Young Generation only. It runs frequently (every few seconds under moderate load) and completes in milliseconds. Application threads pause during Minor GC.

Major GC collects the Old Generation. It runs less frequently but takes longer. Some collectors (CMS, G1) perform major collection mostly concurrently to keep pauses short.

Full GC collects all heap regions including Metaspace. It always stops all application threads. A Full GC is a warning sign: it usually means heap sizing is wrong, there is a memory leak, or the chosen collector cannot keep up with allocation rate.

Stop-the-World events are the pauses where all application threads are suspended. Modern collectors minimize their duration or move work to concurrent phases, but they cannot eliminate stop-the-world entirely for all operations.

Tuning Garbage Collection

GC tuning starts with measurement, not with flag changes. Enable GC logging first:

# Java 9+
-Xlog:gc*:file=gc.log:time,uptime,level,tags

# Java 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

The log shows pause duration, collection frequency, and which generation triggered the collection. Look for Full GC events first; eliminating them is usually the highest-impact fix.

Heap sizing is the first lever. The heap-to-live-set ratio matters more than the raw heap size. A heap that is only 2x the live set triggers constant collection. A ratio of 3x to 5x gives the collector room to run efficiently. Set initial and maximum heap to the same value to avoid resizing pauses:

-Xms4g -Xmx4g

Young Generation sizing controls Minor GC frequency and promotion rate. A larger Young Generation means fewer Minor GCs but longer individual pauses. -XX:NewRatio=2 sets Old:Young at 2:1 (the default).

Choosing the right collector eliminates an entire class of problems. If pause times exceed your SLA on G1, try ZGC or Shenandoah before tuning individual flags. Collector selection:

# G1 (default Java 9+)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200

# ZGC
-XX:+UseZGC

# Shenandoah
-XX:+UseShenandoahGC

GC ergonomics (the JVM's adaptive tuning) adjusts heap region sizes automatically. Trust the defaults initially, then measure before changing individual flags. Every flag added to the command line is something you now own.

Java GC tuning workflow: log, measure, size heap, select collector, verify

Memory Leaks: Causes and Prevention

A memory leak in Java is an object the GC cannot collect because the program still holds a reference to it, even though the program never uses the object again. The heap grows until an OutOfMemoryError terminates the JVM.

3 Common Causes

Unclosed resources. File handles, database connections, and network sockets hold native memory. Forgetting to close them keeps both the Java object and the underlying OS resource alive. Use try-with-resources:

try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    // work here
} // conn and ps closed automatically

Static references. A static field lives for the lifetime of the classloader, which is typically the lifetime of the JVM. Putting a large cache or a collection of per-request objects in a static field holds them forever. Prefer instance fields with a defined lifecycle.

Listeners and callbacks not removed. Registering an event listener, observer, or callback without deregistering it creates a reference from the event source to your object. The event source (often a long-lived singleton) keeps your object reachable indefinitely.

3 Prevention Strategies

Use weak and soft references for caches and optional associations. A WeakReference<T> lets the GC collect the referent when no strong references exist. java.util.WeakHashMap uses weak keys and is suitable for caches keyed on objects with external lifecycles:

Map<Widget, CachedData> cache = new WeakHashMap<>();

Profile before you assume. Tools like VisualVM, JProfiler, and Eclipse MAT take heap dumps and show which object types hold the most memory and what is retaining them. Run a heap dump (-XX:+HeapDumpOnOutOfMemoryError) to capture the state at the moment of failure.

Audit static collections periodically. Search the codebase for static ... List, static ... Map, and static ... Set. Each one is a potential leak if it grows without a bounded eviction policy.

Advanced Collectors: G1, ZGC, and Shenandoah in Depth

G1 divides the heap into up to 2,048 equal regions. Regions are labeled dynamically as Eden, Survivor, or Old rather than being fixed contiguous areas. G1 predicts which regions contain the most garbage (the "garbage-first" part) and collects those during pause phases, meeting the target pause time. Mixed GC cycles collect both Young and Old regions concurrently with the application.

Key G1 flags:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200   # target pause in ms (not a hard limit)
-XX:G1HeapRegionSize=16m   # override region size when default is too small
-XX:G1NewSizePercent=20    # minimum Young Generation percentage
-XX:G1MaxNewSizePercent=60 # maximum Young Generation percentage

ZGC uses load barriers (code injected at object access sites) to track references concurrently. This lets it relocate live objects and update references while the application runs, keeping pause times under 1 ms even on 1 TB heaps. ZGC requires a 64-bit JVM and does not support 32-bit mode.

Shenandoah performs concurrent compaction using a forwarding-pointer trick: it places a forwarding pointer in the object header when it is being relocated, so any thread accessing the object finds the new location transparently. This achieves concurrent compaction without the load barrier overhead ZGC uses, at the cost of slightly higher CPU usage during concurrent phases.

Reference Objects and Finalization

Java provides 4 reference strength levels below the default strong reference.

SoftReference is collected only when the JVM is under memory pressure. Suitable for memory-sensitive caches where you want to keep data if memory allows.

WeakReference is collected in the next GC cycle after the last strong reference drops. Used in WeakHashMap and for associating metadata with objects without preventing their collection.

PhantomReference is collected after finalization. The referent is never directly accessible through the reference; you use a ReferenceQueue to detect when the object has been finalized. Suitable for resource cleanup that must happen after GC but before memory is reused.

Finalization (Object.finalize()) was deprecated in Java 9 and marked for removal. Do not implement finalize(). Use try-with-resources, Cleaner (Java 9+), or PhantomReference with a ReferenceQueue for deterministic cleanup.

// Preferred cleanup pattern (Java 9+)
Cleaner cleaner = Cleaner.create();
Cleaner.Cleanable cleanable = cleaner.register(myObject, () -> {
    // cleanup action runs when myObject becomes phantom-reachable
    nativeResource.close();
});

GC Considerations by Deployment Context

Web applications serving concurrent HTTP requests need low tail latency. A 500 ms Full GC pause that hits during peak traffic shows up directly in p99 response times. G1 with a 100-200 ms target, or ZGC for stricter SLAs, is the right starting point.

Mobile (Android) uses the ART runtime (Android Runtime) with its own GC implementation, not the HotSpot collectors above. ART uses a generational concurrent copying collector. The principles (minimize allocations in hot paths, avoid large allocation spikes) carry over, but the flags and tuning surface are different.

Big data pipelines (Spark, Flink, Hadoop) allocate and discard large objects rapidly. G1 handles these well; ZGC is the choice when pause SLAs are tight even at 100 GB+ heaps. Off-heap memory via ByteBuffer.allocateDirect() or frameworks like Apache Arrow sidesteps GC entirely for bulk data.

Microservices running in containers with 512 MB to 2 GB heap limits benefit from G1 with aggressive ergonomics. Set -XX:MaxRAMPercentage=75.0 instead of -Xmx so the JVM respects the container memory limit automatically:

java -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -jar service.jar

The GC mechanism ties directly to how the Java Memory Model governs thread visibility and happens-before ordering. Java Concurrency and Multithreading Guide covers thread synchronization, the volatile keyword, and the concurrent collections that use GC-aware data structures.

For applying these concepts to real assignments, Advanced Java Data Management Techniques shows how to choose between data structures when memory footprint and GC pressure both matter.

If you have a Java assignment covering memory management, GC tuning, or profiling and want a developer who knows this material to check your work, Java Assignment Help pairs you with a Java specialist. You pay 50% upfront and the remaining 50% after you verify the code runs on your setup.

Share: X / Twitter LinkedIn

Related articles

  • Java

    Java Swing Tutorial for Beginners

    Learn Java Swing from scratch: build your first window, wire button events, master five layout managers, and assemble a working calculator GUI.

    May 24, 2024

  • Java

    Advanced Java Data Management Techniques

    Master advanced Java data management: optimize data structures, handle concurrent access, tune memory, and use serialization and compression in real applications.

    May 3, 2024

  • Java

    Java File I/O: Read, Write, and Manage Files

    A practical guide to Java file I/O: streams, readers and writers, NIO Path and Files, buffering, serialization, and the exceptions that break file code.

    Oct 7, 2023

← All articles

Stuck on a programming assignment?

Get expert help in Java, C++, Python, JavaScript, SQL, and more. We deliver working code with a clear walkthrough so you can understand and defend it.