Garbage Collection in Java
The collector reclaims objects that can no longer be reached. Understanding reachability explains both leaks and pauses.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The core idea
An object is eligible for collection when it is no longer reachable from any GC root. Java traces reachability rather than counting references, which is why two objects pointing at each other are still collected.
GC roots: local variables of live threads, static fields, active threads,
JNI references, objects used as monitors
roots --> [ Service ] --> [ Cache ] --> [ Entry ] reachable, kept
[ Old ] <--> [ Detail ] a cycle, still collectedNote note = new Note("draft");
Note alias = note;
note = null; // still reachable through alias
alias = null; // now unreachable and eligibleMark and sweep
1. Mark walk from the roots, marking everything reachable
2. Sweep reclaim the space used by everything unmarked
3. Compact move survivors together, so allocation stays a pointer bumpGenerational collection
Young generation Old generation
+--------+------+------+ +---------------------+
| Eden | S0 | S1 | | long lived objects |
+--------+------+------+ +---------------------+
new copy survivors promoted after n cycles| Collection | Covers | Cost |
|---|---|---|
| Minor | The young generation | Frequent and cheap |
| Major or full | The whole heap | Rare and expensive |
Most objects die young, so collecting a small young area often is cheap: only survivors are copied, and dead objects cost nothing at all. This is why creating short lived objects in Java is far less costly than intuition suggests.
The collectors
| Collector | Flag | Optimises for |
|---|---|---|
| Serial | -XX:+UseSerialGC | Small heaps, one processor |
| Parallel | -XX:+UseParallelGC | Throughput, pauses acceptable |
| G1 | -XX:+UseG1GC | Balance; the default on most modern JVMs |
| ZGC | -XX:+UseZGC | Very low pauses on large heaps |
| Shenandoah | -XX:+UseShenandoahGC | Low pauses, concurrent compaction |
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx4g -jar app.jarG1 divides the heap into regions and collects those with the most garbage first, aiming at a pause target you specify. ZGC and Shenandoah keep pauses to a few milliseconds even on very large heaps, at some cost in throughput.
The throughput and latency trade off
Throughput collector: ####pause#### work work work ####pause####
Low latency collector: |p| work work |p| work work |p| work (concurrent, more overhead)There is no collector that is best at everything. A batch job should prefer throughput; an interactive service should prefer short pauses.
System.gc is a request
System.gc(); // a suggestion; the JVM may ignore it entirelyCalling it in application code is almost always a mistake. It can force an expensive full collection at exactly the wrong moment, and it never guarantees anything.
Reference types
Note strong = new Note("kept"); // never collected while reachable
SoftReference<Note> soft = new SoftReference<>(new Note("cache"));
WeakReference<Note> weak = new WeakReference<>(new Note("mapping"));
PhantomReference<Note> phantom = new PhantomReference<>(note, queue);
Note value = weak.get(); // may be null at any time| Type | Collected when | Use for |
|---|---|---|
| Strong | Unreachable | Everything ordinary |
| Soft | Memory is running low | Caches that may be discarded |
| Weak | At the next collection | Canonical maps, listener registries |
| Phantom | After finalisation | Cleanup notification |
Map<Key, Value> cache = new WeakHashMap<>();
// An entry disappears once its key is unreachable elsewhereObserving collection
java -Xlog:gc -jar app.jar
java -Xlog:gc*:file=gc.log:time,uptime -jar app.jar
jstat -gc <pid> 1s[0.412s][info][gc] GC(0) Pause Young (Normal) 24M->4M(256M) 6.221msRead it as: the young generation went from 24 MB used to 4 MB used, in a heap of 256 MB, taking about 6 milliseconds. Healthy logs show the heap dropping back to a similar level each time. A floor that keeps rising is the signature of a leak.
Reducing pressure
- Avoid creating objects in the hottest loops, especially through boxing.
- Use primitive streams and primitive collections where it matters.
- Size collections up front so they do not repeatedly grow and copy.
- Reuse buffers rather than allocating per call.
- Prefer short lived objects to a hand written object pool; the young generation is already fast.
// Allocates a wrapper per iteration
Long total = 0L;
for (long i = 0; i < 1_000_000; i++) { total += i; }
// No allocation at all
long total = 0;
for (long i = 0; i < 1_000_000; i++) { total += i; }finalize is gone
Object.finalize() was deprecated and then removed. It ran unpredictably, could resurrect objects and delayed collection. Use try with resources for deterministic cleanup, and java.lang.ref.Cleaner only as a last resort safety net over a native resource.
Common mistakes
- Calling
System.gc()in application code. - Relying on collection to release files, sockets or connections.
- Assuming
nullassignment frees memory immediately. - Building an object pool for ordinary objects, which is usually slower than allocating.
- Tuning collector flags without first reading a GC log.
Practice
- Explain, in terms of reachability, why two objects referring to each other are still collected.
- Enable GC logging for a small program and read one line of output.
- When would a
WeakReferencebe the right choice over a strong one? - Why is allocating many short lived objects cheaper than it sounds?
- Describe how a GC log looks when the application has a genuine leak.
Conclusion
Collection is driven by reachability from roots, organised around the fact that most objects die young. Choose a collector to match throughput or latency, read the log before tuning, and never depend on when collection happens.