Java Garbage Collection Basics and Object Lifecycle
Introduction
In Java you create objects with
(Note - This tutorial builds on Stack vs Heap Memory and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References.) The lifecycle of an object
OUTPUT of the above code
The
A useful consequence: objects which refer only to each other - a cycle - are still collected once nothing reachable points into the cycle. Reference counting could never free them, but reachability does.
OUTPUT of the above code (collection timing can vary)
A
Watching the garbage collector - GC logging
The
OUTPUT with -Xmx32m -Xlog:gc (timings and sizes vary)
Explanation of the output
What you cannot control - System.gc() and finalize()
new, but you never delete them. Instead, the JVM's garbage collector finds objects which can no longer be used and frees their memory automatically. This removes a whole class of bugs - freeing memory too early, or forgetting to free it - that plague languages with manual memory management. This tutorial explains Java garbage collection basics and the lifecycle of an object. We will start with the lifecycle of an object, from creation to collection. We will then see how the garbage collector decides what is garbage, using reachability. Next we will watch the garbage collector at work with GC logging, and look at generations and the collectors available. Lastly, we will look at what you can and cannot control - System.gc(), finalization - and how memory leaks still happen.(Note - This tutorial builds on Stack vs Heap Memory and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References.) The lifecycle of an object
- Created -
newallocates memory on the heap, sets the fields to their default values, and runs the constructor. - In use (reachable) - the program holds at least one reference to it, directly or through other objects.
- Unreachable - the last reference to it is gone: the variable went out of scope, was set to
nullor pointed at another object, or the object holding it became unreachable too. The program can never use the object again. - Collected - at some later point, the garbage collector reclaims its memory. When this happens is up to the JVM.
Ways an object becomes unreachable
//Lifecycle.java
import java.util.ArrayList;
import java.util.List;
public class Lifecycle {
static List<String> cache = new ArrayList<>();
static void makeTemporary() {
StringBuilder temp = new StringBuilder("only used here"); // unreachable when the method returns
System.out.println("temp length: " + temp.length());
}
public static void main(String[] args) {
makeTemporary(); // 1. local variable went out of scope
String report = new String("first report");
report = new String("second report"); // 2. first object lost its only reference
String draft = new String("draft");
draft = null; // 3. reference cleared explicitly
cache.add(new String("cached")); // reachable from a static field - stays alive
System.out.println("report = " + report + ", draft = " + draft + ", cache = " + cache);
}
}
temp length: 14 report = second report, draft = null, cache = [cached]
StringBuilder, the "first report" object and the "draft" object are now unreachable, and eligible for collection. The "cached" string stays alive for as long as the static cache list holds it.
Reachability - how the GC finds garbage
The garbage collector does not count references. It starts from a set of GC roots - local variables in the stack frames of every running thread, static fields, and a few JVM-internal references - and follows every reference from them, marking each object it reaches. Anything not marked is unreachable, and is garbage.A useful consequence: objects which refer only to each other - a cycle - are still collected once nothing reachable points into the cycle. Reference counting could never free them, but reachability does.
Objects in a cycle are still collected
//CycleDemo.java
import java.lang.ref.WeakReference;
public class CycleDemo {
static class Node {
Node next;
byte[] payload = new byte[1024 * 1024];
}
public static void main(String[] args) throws InterruptedException {
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a; // a and b refer to each other
WeakReference<Node> watchA = new WeakReference<>(a); // watches a without keeping it alive
System.out.println("before: a is alive = " + (watchA.get() != null));
a = null;
b = null; // the cycle is now unreachable from any root
System.gc(); // a request, not a command - see below
Thread.sleep(200);
System.out.println("after : a is alive = " + (watchA.get() != null));
}
}
before: a is alive = true after : a is alive = false
WeakReference lets us watch an object without keeping it reachable. After the last strong references were cleared, the garbage collector freed both nodes, even though each still pointed at the other.
-Xlog:gc option makes the JVM print a line for each collection. This program creates a stream of short-lived objects in a small heap.
Creating many short-lived objects
//GcLog.java
public class GcLog {
public static void main(String[] args) {
long total = 0;
for (int i = 0; i < 300; i++) {
byte[] chunk = new byte[200 * 1024]; // 200 KB, garbage after each iteration
total += chunk.length;
}
System.out.println("Allocated " + total / (1024 * 1024) + " MB in total");
}
}
[0.011s][info][gc] Using G1 [0.056s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 18M->1M(32M) 1.413ms [0.057s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 14M->1M(32M) 0.366ms [0.059s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 18M->1M(32M) 0.308ms Allocated 58 MB in total
- The program allocated far more memory in total than the 32 MB heap, yet never ran out - each collection freed the chunks which were no longer reachable.
- Each
Pause Youngline is one collection of the young generation, showing heap usage before and after it (before->after(total)), and how long the application was paused - a few milliseconds for the first collection, and under a millisecond for the ones after it. After each collection only about 1 MB was still in use. - The first line names the collector in use - G1, the default since Java 9.
- Young generation: new objects are created here. It is collected often and quickly, because most of it is garbage - only the few survivors are copied.
- Old generation: objects which survive several young collections are promoted here. It is collected less often.
- G1 (
-XX:+UseG1GC) - the default: a good balance of throughput and short pauses. - ZGC (
-XX:+UseZGC) and Shenandoah - very short pauses even with huge heaps; see ZGC and Shenandoah in Java 15Click to read tutorial on ZGC and Shenandoah. - Parallel (
-XX:+UseParallelGC) - maximum throughput, longer pauses; and Serial for small single-core environments.
- System.gc() only requests a collection; the JVM may ignore it, and it can be disabled entirely with
-XX:+DisableExplicitGC. Calling it in application code usually makes performance worse. It is used above only to make the demonstration predictable. - finalize() was meant to run cleanup code before an object is collected, but there was no guarantee when - or whether - it would run. It is deprecated for removal since Java 18. Never rely on garbage collection to release files, connections or locks; close them explicitly with try-with-resourcesClick to read tutorial: Java try-with-resources and AutoCloseable Explained. For cleanup tied to an object's collection,
java.lang.ref.Cleaneris the modern replacement.
OutOfMemoryError, as shown in Stack vs HeapClick to read tutorial: Stack vs Heap Memory in Java and Object References. Common causes are -- static collections and caches which only ever grow - give caches a size limit or an expiry;
- listeners and callbacks which are registered but never removed;
- long-lived objects holding references to short-lived ones, such as a session object keeping every request it has seen.
jcmd <pid> GC.heap_dump) show which objects fill the heap and what keeps them reachable.
Summary
In this tutorial we first followed an object through its lifecycle - created, reachable, unreachable and collected - and saw the ways an object loses its last reference. We then learned that the garbage collector works by reachability from GC roots, which is why even cycles of objects are collected. Next we watched collections happen with -Xlog:gc, and looked at the young and old generations and the G1, ZGC, Shenandoah and Parallel collectors. We then saw why System.gc() and finalize() should not be relied on, and lastly how memory leaks still happen when unneeded objects stay reachable. In the next tutorial, we will deal with the most common runtime error in Java - null and NullPointerExceptionClick to read tutorial: null and NullPointerException in Java: How to Avoid It.