What is Java garbage collection?
In Java we create objects with new; and donot need to call
delete. The garbage collector (GC) in a JVM subsystem
that finds objects no longer reachable from running code and reclaims
their heap memory automatically.
Object is alive(not garbage collected) if a chain of references exists
from GC roots(thread stacks, static fields, JNI handles, etc) to that
object. Everything else is garbage.
GC runs inside the JVM
process on the
heap
Stack frames and metaspace are managed separately; GC focuses on object
instances in the heap.
See
Heap Area(Eden, S0, S1)
Heap & generations
GC runs on the JVM heap: young generation (Eden, Survivor S0/S1) and old generation (tenured). See Java memory layout for the full diagram.
How the garbage collector works (high level)
Different collectors implement these steps differently (parallel threads, concurrent marking, region-based collection). All share the goal: free memory for new allocations while keeping pauses acceptable.
Small example — Eden and minor GC
Java Garbage Collection
|
C++ Memory Free
|
When exactly does GC run?
Eden fills up OR
Heap usage crosses thresholds (memory Pressure)
OR
Allocation rate is high OR
JVM decides it's worthwhile OR
Sometimes when System.gc() is requested (only a hint; the JVM may ignore
it)
Stop-the-world (STW) pause (millisec)
Means all application threads that use the heap are halted while the GC
performs work (copying, marking roots, compaction). During STW your code
does not run — API latency spikes if the pause is long.
Modern GCs cannot eliminate the STW compltely, even they collected
garbage parallel to execution.
Concurrent mark phase
In collectors like G1 or ZGC, in concurrent mark
phase GC walks the heap to identify live objects while application
threads mostly keep running. Only brief STW checkpoints bracket this
phase (initial mark, remark in G1).
Concurrent mark phase duration depends on: 1. Heap
size(more regions/pages to scan), 2. Live set size(more live objects →
longer mark).
Can Java have memory leaks?
C++ have new() and delete(), and if we forget to delete() there can be memory leak, but Java does not have delete() so when memory leak can happen in Java?
1. Objects kept on getting allocated and their References not released
static List >byte[]< cache = new ArrayList><();
while (true) {
cache.add(new byte[1024 * 1024]); // 1 MB each
}
The list keeps references forever, so GC cannot free the arrays. Eventually you'll get:
java.lang.OutOfMemoryError: Java heap space
Allocation rate vs GC reclaim rate
Allocation rate > GC reclaim rate
↓
Heap fills up
↓
Frequent GC
↓
Long GC pauses
↓
OutOfMemoryError
Major and modern garbage collectors
| Collector | Era | Style | Typical use |
|---|---|---|---|
| Serial GC | Classic | Single-threaded, stop-the-world | Small heaps, client apps |
| Parallel GC (Throughput) | Classic | Multi-threaded STW, maximize throughput | Batch jobs, Java 8 default on server |
| CMS | Older enterprise | Concurrent old-gen marking (deprecated) | Legacy systems (removed in modern JDKs) |
| G1 GC | Modern default | Region-based, predictable pause target | Java 9+ default; enterprise services |
| ZGC | Modern low-latency | Concurrent, sub-ms pauses (typical goal) | Large heaps, latency-sensitive apps |
| Shenandoah | Modern low-latency | Concurrent compaction | Similar niche to ZGC (OpenJDK / some vendors) |
Example — G1 GC (Garbage-First)
G1 divides the heap into equal-sized regions (1–32 MB). Eden, survivors,
and old gen are logical sets of regions — not one fixed contiguous
block.
Why banks choose G1
Banks choose G1 because
1. G1 gives predictable pause times(hence
preferred due to debugging). MaxGCPauseMillis
2. Banks need regulatory audit trails, 24×7 availability, consistent
response times for customer channels, and large long-lived caches
(accounts, rates) in old generation — GC choice directly affects
peak-hour latency and outage risk.
How G1 works
Young collection: collect all Eden + survivor regions (stop-the-world, parallel).
Concurrent mark: mark live objects across heap while app runs (mostly concurrent).
Mixed collection: collect young regions + some old regions with most garbage (“garbage-first”).
# Enable G1 (default on Java 9+ for server-class machines)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xms4g -Xmx4g
Moving Objects from Eden to old generation
Tenuring / Promotion: Each object in young gen has an age
(survived GC count). When age ≥ MaxTenuringThreshold (or
survivor space is full), live objects are promoted to
old generation.
Promotion failure (also called promotion denied): During
minor GC, the collector needs free space in old gen to receive promoted
objects. If old gen is full or too fragmented to fit them, promotion
fails. The JVM may then trigger a
full GC (expensive)
Common causes of Promotion Failure old gen too small
(-Xmx mostly eaten by tenured objects), memory leak
(objects never become garbage), spike of long-lived allocations,
fragmentation under certain collectors.
Mitigation to Promotion: increase heap, fix leaks, tune tenuring
threshold, use G1 mixed cycles earlier, or switch to ZGC for very large
heaps; analyze with GC logs and heap dumps.
Key JVM parameters that affect GC
| Flag | Meaning |
|---|---|
-Xms / -Xmx |
Initial and max heap size |
-XX:+UseG1GC |
Select G1 collector |
-XX:MaxGCPauseMillis |
G1 pause time goal (soft target) |
-XX:NewRatio |
Old:young ratio (generational collectors) |
-XX:SurvivorRatio |
Eden vs one survivor size |
-XX:MaxTenuringThreshold |
GCs survived before promotion to old gen |
-XX:+UseZGC |
Enable ZGC (Java 15+ production, Java 21+ generational ZGC) |
-Xlog:gc* |
Unified GC logging (Java 9+) |
Monitoring GC metrics
1. GC logs:
-Xlog:gc*:file=gc.log:time,uptime,level,tags
(Java 9+). Shows pause times, phases, promotion, heap before/after.
2. JFR (Java Flight Recorder) — low-overhead events:
jdk.GCConfiguration, jdk.GCPhasePause,
allocation profiling.
3. JMX / VisualVM / JDK Mission Control — live heap, GC count, GC time,
old-gen usage.
4. Micrometer + Prometheus/Grafana — export
jvm.gc.pause, jvm.memory.used from Spring Boot
Actuator.
Metrics to watch:
GC pause time (p50, p99, max STW)
GC frequency (young vs old/mixed)
Heap used after full GC (trend up = leak?)
Promotion rate and promotion failures
Allocation rate (MB/s into Eden)
Tuning / demo GC (hands-on)
1. Run with verbose GC logging:
java -Xms512m -Xmx512m -Xlog:gc*:stdout:time,uptime GcDemo
2. Force GC in a demo (not for production):
System.gc(); // hint only — JVM may ignore; used in tutorials
3. Compare collectors on same workload:
java -XX:+UseG1GC -Xlog:gc*:file=g1.log MyApp
java -XX:+UseParallelGC -Xlog:gc*:file=parallel.log MyApp
java -XX:+UseZGC -Xlog:gc*:file=zgc.log MyApp
4. Tune G1 for banking-style latency target:
-Xms8g -Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+ParallelRefProcEnabled
-Xlog:gc*,gc+heap=debug:file=gc.log:time,tags
Process: measure baseline → change one flag at a time → compare p99 latency and GC logs under load test (JMeter/Gatling) → avoid tuning without production-like traffic.