---
topic: dev-practices
author: Crashtech Editorial
published: Nov 14, 2026 · read: 4 min
---

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.

– –

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 costWhat you feel
Pause timeLatency spikes, missed deadlines, tail latency
ThroughputPercentage of CPU spent collecting instead of working
FootprintExtra 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

Serial small and single-threaded
One thread, simple, minimal overhead. Fine for small heaps, tools and containers with a single core. Pauses grow with heap size.
Parallel throughput first
Uses many threads to collect during the pause. Maximises work done per CPU second, accepting longer pauses. Good for batch jobs and offline processing.
G1 balanced default
Splits the heap into regions, collects the ones with the most garbage first, and aims for a pause-time target. The default in modern JDKs, and a sound starting point for services.
ZGC very low pauses, big heaps
Does nearly all work concurrently, so pauses stay very short even on multi-terabyte heaps. It costs some throughput and memory headroom.

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.

When this bites

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:

SettingWhat it controlsUse it when
GOGCHeap growth allowed over live data before the next cycle. Default 100 means the heap may doubleSpare memory: raise it for fewer cycles. Tight memory: lower it
GOMEMLIMITA soft total memory ceiling. The collector works harder near itContainers with a fixed memory limit
GOGC=200 ./service                # fewer collections, more memory
GOMEMLIMIT=900MiB ./service       # stay inside a 1 GiB container
Advertisement

Choosing by constraint

Your constraintLean toward
Strict tail-latency target, large heapZGC (Java)
General web serviceG1 (Java), defaults in Go
Offline batch, maximise throughputParallel (Java)
Tiny process or toolSerial (Java)
Deterministic frees, no pausesPython refcounting, with cycle awareness
Memory-capped containerGo 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.

Advertisement

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.

Sources & further reading

/* Comments */