Garbage collectors are like janitors with very different schedules: one works alone, one brings a crew, one tries to clean quietly in the background, and one rearranges the whole room so the mess is easier to remove.
Question: What is the difference between Serial GC, Parallel GC, CMS, and G1 in Java?
Answer: These are different Java garbage collectors with different goals. Serial GC uses one thread and is simple, Parallel GC uses many threads to maximize throughput, CMS tries to reduce pause times by doing most old-generation work concurrently, and G1 is a region-based collector that aims for predictable pauses while still keeping good throughput. On modern Java, G1 is the default, and CMS is historical because it was removed in JDK 14.
Interview-Ready Answer: I’d explain it this way: Serial GC is the simplest collector and stops the world with one thread, so it’s best for tiny heaps or single-core situations. Parallel GC also stops the world, but uses multiple threads to finish faster, so it’s usually the throughput choice. CMS was designed for lower pause times by doing old-gen marking and sweeping concurrently, but it can fragment the heap and it was removed in JDK 14. G1 is the modern default; it splits the heap into regions, does concurrent marking, and then evacuates selected regions to keep pauses more predictable, with a typical pause target like 200 ms rather than a hard guarantee.
Java allocates objects on the heap. Some objects die quickly, like temporary strings, request objects, and loop-local buffers. A garbage collector is the JVM’s memory manager: it finds objects that are no longer reachable and frees their memory. In interviews, the key trade-off is always throughput versus pause time. Throughput means how much useful work the app does overall; pause time means how long the app must stop while GC runs. A stop-the-world pause means application threads are temporarily frozen.
| Collector | Pause style | Goal | Weak spot | Best fit |
|---|---|---|---|---|
| Serial | STW, 1 thread | Simplicity | Slow on big heaps | Tiny apps |
| Parallel | STW, many threads | Throughput | Long pauses | Batch jobs |
| CMS | Mostly concurrent | Lower pauses | Fragmentation | Legacy low-latency |
| G1 | Mixed, region-based | Predictable pauses | Needs headroom | General purpose |
Use Serial GC when the heap is small and simplicity matters more than raw speed. Use Parallel GC when you want maximum throughput and can tolerate longer pauses, such as data processing or offline batch work. CMS used to be chosen for low-pause systems, but it is now mostly an interview topic because it was removed in JDK 14. Use G1 for most modern server applications because it balances latency and throughput better than the older collectors.
The work each collector does is roughly proportional to the amount of live data it must scan or move. Parallel GC reduces elapsed pause time by using more CPU threads, but not the total amount of work. G1 adds bookkeeping for regions, remembered sets, and concurrent marking, so it has more overhead than Serial or Parallel, but it buys better pause predictability. A good memory detail: G1’s pause goal is often set with -XX:MaxGCPauseMillis, whose default target is commonly 200 ms, but that is only a goal, not a promise. Another classic G1 detail is that objects larger than half a region are treated as humongous, which can affect fragmentation and compaction behavior.
Memory hook: imagine a house clean-up team: Serial is one cleaner, Parallel is a full crew, CMS is a quiet cleaner who works around people but leaves messy corners, and G1 is a project manager who sorts the house into rooms and cleans the worst rooms first.
Real-World Story: A checkout service at a retail company starts timing out during a holiday sale. The service uses a lot of short-lived request objects, but it also keeps some customer and pricing data alive for longer. On older JVMs, the team tried CMS to keep pauses low. It worked for a while, then traffic grew and the heap became fragmented. The logs started showing phrases like concurrent mode failure and occasional long Full GC pauses. Users saw slow checkouts, retries from the load balancer, and spikes in 504 Gateway Timeout responses. When the team switched to G1, the collector compacted regions during evacuation, pauses became more predictable, and the p99 latency graph flattened out. The important lesson: a collector choice is not just a JVM setting; it changes user-visible behavior under load.
What goes wrong when you misunderstand it: teams sometimes pick a collector because it sounds “fast” instead of matching it to the workload. A throughput collector on an API server can create long pauses, while a low-pause collector on a batch job may waste CPU and reduce total work completed. The symptom is usually not a crash first; it is latency spikes, timeout logs, and only later an emergency heap-size increase.
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
public class GcWorkloadDemo {
public static void main(String[] args) {
boolean stress = args.length > 0 && "stress".equalsIgnoreCase(args[0]);
// Default mode creates mostly short-lived garbage.
// Stress mode keeps more objects alive and can trigger OutOfMemoryError on small heaps.
int rounds = stress ? 4_000 : 3_000;
int blockKb = stress ? 256 : 64;
int retainEvery = stress ? 2 : 128;
int maxSurvivors = stress ? 1_024 : 64;
printStatus("Before");
try {
runWorkload(rounds, blockKb, retainEvery, maxSurvivors);
} catch (IllegalArgumentException e) {
System.err.println("Bad input: " + e.getMessage());
return;
} catch (OutOfMemoryError e) {
System.err.println("Caught OutOfMemoryError: " + e.getMessage());
System.err.println("This is the failure path when too many objects survive and the heap cannot keep up.");
return;
}
// System.gc() is only a hint. Different collectors may treat it differently,
// which is exactly why GC behavior should be observed, not assumed.
System.gc();
sleep(200);
printStatus("After");
}
private static void runWorkload(int rounds, int blockKb, int retainEvery, int maxSurvivors) {
if (rounds < 1 || blockKb < 1 || retainEvery < 1 || maxSurvivors < 1) {
throw new IllegalArgumentException("all parameters must be positive");
}
List<byte[]> survivors = new ArrayList<>(Math.min(maxSurvivors, 16));
long checksum = 0;
for (int i = 1; i <= rounds; i++) {
byte[] payload = new byte[blockKb * 1024];
// Touch the array so the allocation is real work, not just dead code.
payload[0] = (byte) i;
payload[payload.length - 1] = (byte) (i >>> 1);
checksum += payload[0] + payload[payload.length - 1];
// Most objects die quickly, which is what young-generation GC is designed for.
// Every Nth object survives longer, which creates old-gen pressure.
if (i % retainEvery == 0) {
survivors.add(payload);
if (survivors.size() > maxSurvivors) {
survivors.remove(0);
}
}
if (i % 500 == 0) {
printStatus("Round " + i);
}
}
System.out.println("Checksum: " + checksum);
System.out.println("Retained survivors: " + survivors.size());
}
private static void printStatus(String label) {
Runtime rt = Runtime.getRuntime();
long usedMb = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
long totalMb = rt.totalMemory() / (1024 * 1024);
long maxMb = rt.maxMemory() / (1024 * 1024);
System.out.println("\n== " + label + " ==");
System.out.println("Heap used/total/max (MB): " + usedMb + "/" + totalMb + "/" + maxMb);
long totalCollections = 0;
long totalTimeMs = 0;
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
long count = bean.getCollectionCount();
long time = bean.getCollectionTime();
totalCollections += Math.max(count, 0);
totalTimeMs += Math.max(time, 0);
System.out.println("GC bean: " + bean.getName() + " | collections=" + count + " | timeMs=" + time);
}
System.out.println("Total collections/time: " + totalCollections + "/" + totalTimeMs + "ms");
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Follow-up & Tricky Questions:
-XX:MaxGCPauseMillis is a goal used by the collector’s heuristics, not a hard real-time guarantee, so a badly tuned heap or too much live data can still cause longer pauses.Common Mistakes:
Memory Hook: One cleaner, many cleaners, quiet cleaner, smart room planner = Serial, Parallel, CMS, G1.
Cheat Sheet:
Serial GC = one thread, stop-the-world, tiny heaps.Parallel GC = many threads, stop-the-world, best throughput.CMS = concurrent old-gen cleanup, low pauses, no compaction, removed in JDK 14.G1 = region-based, concurrent marking, evacuation, predictable pauses, default since Java 9.Practice Tasks:
-XX:+UseSerialGC and -XX:+UseParallelGC, and compare the printed GC counts.-Xmx64m and the stress argument to see how long-lived objects can create pressure.