Learn how to approach JVM garbage collection in Clojure with current guidance: fix allocation shape first, read GC logs, choose collector goals deliberately, and avoid obsolete cargo-cult flags.
GC tuning: Adjusting heap and collector behavior so garbage collection supports the workload instead of becoming a visible source of pauses or waste.
Garbage collection is not an enemy to defeat. It is the JVM’s memory-management strategy, and in most Clojure systems it works well until the application creates more pressure than the default behavior can absorb cleanly.
That means the first GC question is usually not “which flag should I set?” It is:
If those answers are not clear, GC tuning often turns into cargo culting.
Most painful GC behavior originates in application shape:
If those patterns stay unchanged, collector tuning can help only so much.
So the usual order is:
Modern JVM collectors make different trade-offs:
For many current server-side Clojure applications, the best starting point is the modern default collector behavior with minimal extra flags, not a pile of legacy settings copied from old blog posts.
If the workload is especially sensitive to pause time, then a lower-latency collector such as ZGC may deserve evaluation. If throughput matters more than pause targets, a different default may still be preferable.
The key is to tune toward a workload goal, not toward collector fandom.
One of the most common problems in JVM tuning is stale advice that lingers long after the platform changed.
Examples of outdated habits:
Current JVMs are better than older guides imply. Start simpler.
GC tuning without logs is guesswork. A good starting point is:
1java \
2 -Xms4g -Xmx4g \
3 -Xlog:gc*:file=gc.log:time,uptime,level,tags \
4 -jar app.jar
From the logs, you want to learn:
The goal is not just to collect GC data. The goal is to decide whether the problem is:
Heap sizing is a policy decision, not just a memory maximum. Too small a heap can cause frequent collection. Too large a heap can increase recovery work and hide live-set problems.
Useful questions:
If the process runs in a container, those deployment limits matter just as much as the JVM flags.
This often improves symptoms only temporarily.
Many old settings were designed for collectors or JDK versions that no longer behave the same way.
Tail pauses and frequency matter too.
More heap can hide retained-data problems without truly solving them.
Treat GC tuning as the last third of memory optimization, not the first. Fix avoidable allocation and retention issues first, gather GC logs, start with simple modern collector settings, and tune only toward a specific workload goal such as throughput or low pause time. In Clojure, the strongest GC improvements usually begin in data flow and allocation shape, not in flag collections copied from old JVM folklore.