Garbage Collection Compared: Java's Collectors, Python's Reference Counts, Go's Concurrent Sweep
Latency, throughput and memory footprint decide which collector fits. How Serial, G1 and ZGC differ, why Python needs two mechanisms, and how Go tunes GC.
On this page
Garbage collection is invisible when it works and blamed for every latency spike when it does not. The useful question is never “which collector is best” but “which cost can this service afford to pay”: longer pauses, lower throughput, or more memory.
What a collector actually does
It finds objects the program can no longer reach and reclaims their memory. Most tracing collectors do it in phases: mark everything reachable from roots (stacks, globals), then sweep or compact the rest. The expensive part is doing that while the program keeps running and changing the object graph, so some collectors pause the program (stop the world) for part of the work and others do it concurrently.
| The cost | What you feel |
|---|---|
| Pause time | Latency spikes, missed deadlines, tail latency |
| Throughput | Percentage of CPU spent collecting instead of working |
| Footprint | Extra memory needed so the collector can keep up |
You cannot minimise all three. Each collector picks a point on that triangle.
Java: a menu, by workload
The older CMS (Concurrent Mark-Sweep) collector aimed at low pauses, but it was deprecated and then removed from the JDK. If a guide tells you to enable it, the guide is old, and the modern answers are G1 or ZGC.
Practical Java advice:
java -XX:+UseG1GC -Xmx4g -XX:MaxGCPauseMillis=200 -jar app.jar # balanced
java -XX:+UseZGC -Xmx32g -jar app.jar # low latency, large heap
java -XX:+UseParallelGC -jar batch.jar # throughput
Turn on GC logging (-Xlog:gc) before you tune anything, and read it before you change a flag.
Python: two mechanisms
Reference counting is the primary one. Every object counts how many references point at it. When the count drops to zero, memory is freed immediately and deterministically, with no pause scanning the heap. The cost is bookkeeping on every assignment.
Its blind spot is cycles:
a = Node(); b = Node()
a.other = b
b.other = a # a and b reference each other
del a, b # counts never reach zero, yet nothing can reach them
The second mechanism, the cyclic garbage collector, periodically finds groups of objects that reference only each other and are unreachable, and frees them. It is generational: young objects are checked more often than old ones. The gc module lets you inspect, tune thresholds, or trigger a collection.
Cycles holding large objects (caches with back-references, parent and child links) can sit in memory until the cycle collector runs. Break cycles with weak references where you can, and do not rely on __del__ to free scarce resources.
Go: concurrent mark and sweep with two knobs
Go’s collector is a concurrent mark-and-sweep design. Most marking runs alongside your goroutines, so stop-the-world pauses are brief. Go does not give you collector choices. It gives you two settings:
| Setting | What it controls | Use it when |
|---|---|---|
| GOGC | Heap growth allowed over live data before the next cycle. Default 100 means the heap may double | Spare memory: raise it for fewer cycles. Tight memory: lower it |
| GOMEMLIMIT | A soft total memory ceiling. The collector works harder near it | Containers with a fixed memory limit |
GOGC=200 ./service # fewer collections, more memory
GOMEMLIMIT=900MiB ./service # stay inside a 1 GiB container
Choosing by constraint
| Your constraint | Lean toward |
|---|---|
| Strict tail-latency target, large heap | ZGC (Java) |
| General web service | G1 (Java), defaults in Go |
| Offline batch, maximise throughput | Parallel (Java) |
| Tiny process or tool | Serial (Java) |
| Deterministic frees, no pauses | Python refcounting, with cycle awareness |
| Memory-capped container | Go GOMEMLIMIT, Java heap sized with headroom |
Before you tune, measure
Do
Log GC activity, record pause times and allocation rate, reproduce the problem under load, then change one setting at a time. Often the best fix is allocating less: reuse buffers, avoid creating garbage in hot loops.
Don't
Copy flags from a blog post written for a different JDK, heap size and workload. Many of them are obsolete or actively harmful on current versions.
Takeaway
Garbage collection is a three-way trade between pauses, throughput and memory. Java lets you choose a collector, Python pairs immediate reference counting with a cycle detector, and Go keeps one concurrent collector with two tuning knobs. Decide which cost your service cannot afford, pick the matching strategy, and verify with GC logs rather than folklore.
Frequently asked questions
What is a stop-the-world pause?
A moment when the garbage collector halts all application threads so it can safely inspect or move objects. Pause length is what users feel as a hiccup. Modern collectors such as ZGC and Go's collector do most of their work concurrently so pauses stay very short.
Why does Python need a cyclic garbage collector as well as reference counting?
Reference counting frees an object when its count reaches zero, but two objects that reference each other never reach zero even when nothing else can reach them. The cyclic collector periodically finds such unreachable groups and frees them, covering what counting cannot.
Which Java garbage collector should I use?
For most services the default G1 is a sound starting point, balancing throughput and pause times. Choose ZGC when you need very low pauses on large heaps, Parallel when throughput of batch work matters more than pauses, and Serial for tiny or single-threaded programs.
What do GOGC and GOMEMLIMIT do in Go?
GOGC sets how much the heap may grow past the live data before the next collection, trading memory for less frequent GC. GOMEMLIMIT sets a soft memory ceiling that makes the collector work harder as usage nears it, which helps in containers with fixed memory.
/* Comments */
Comments are offline right now — we reconnect automatically, nothing is lost.