Managing Garbage Collection for Better Clojure Performance

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:

  • what is allocating so much
  • what is staying live so long
  • what pause behavior is unacceptable for this workload

If those answers are not clear, GC tuning often turns into cargo culting.

Allocation Shape Comes Before Collector Tuning

Most painful GC behavior originates in application shape:

  • large intermediate collections
  • repeated map or string reshaping
  • unbounded queues or caches
  • boxed numeric churn
  • data retained longer than intended

If those patterns stay unchanged, collector tuning can help only so much.

So the usual order is:

  1. inspect allocation and retained-size behavior
  2. reduce avoidable churn
  3. size the heap sanely
  4. tune collector behavior only after the workload is understood

Match the Collector to the Goal

Modern JVM collectors make different trade-offs:

  • throughput-oriented collection for batch or less latency-sensitive workloads
  • general-purpose balanced collection for many server applications
  • lower-pause collectors for tighter latency targets

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.

Some Old GC Advice Is Now Outdated

One of the most common problems in JVM tuning is stale advice that lingers long after the platform changed.

Examples of outdated habits:

  • carrying forward old CMS-era tuning assumptions
  • pinning dozens of young-generation flags without evidence
  • treating heap growth alone as a GC fix
  • copying a large flag set from another application with different latency goals

Current JVMs are better than older guides imply. Start simpler.

GC Logs Are Essential

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:

  • how often collections occur
  • how long pauses last
  • whether the heap is recovering enough after collections
  • whether allocation pressure is driving frequent young collections
  • whether the old generation or long-lived set is the real issue

The goal is not just to collect GC data. The goal is to decide whether the problem is:

  • heap sizing
  • collector choice
  • allocation rate
  • retained live set

Size the Heap Deliberately

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:

  • what is the steady live set
  • how bursty is allocation
  • what pause budget is acceptable
  • how much memory is actually available in the deployment environment

If the process runs in a container, those deployment limits matter just as much as the JVM flags.

Common Failure Modes

Tuning the Collector Before Reducing Allocation Pressure

This often improves symptoms only temporarily.

Copying Legacy Flag Bundles Blindly

Many old settings were designed for collectors or JDK versions that no longer behave the same way.

Looking Only at Average Pause Time

Tail pauses and frequency matter too.

Confusing Heap Growth with Healthy Headroom

More heap can hide retained-data problems without truly solving them.

Practical Heuristics

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.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026