Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Shenandoah for Cassandra

Shenandoah evacuates objects concurrently with application threads and compacts as it goes, which keeps the remaining pauses short at heap sizes where a stop-the-world collection would not be. Shenandoah appears in no options file on Cassandra 5.0, 4.1 or 4.0: only CMS and G1 do, no Apache Cassandra ticket covers Shenandoah, and project continuous integration does not exercise it. Every setting on this page is therefore operator-supplied, and behaviour under a given workload is established by local measurement alone.

AxonOps recommends Shenandoah over G1 for Cassandra, with -XX:ShenandoahGCHeuristics=compact in place of the JDK default adaptive. A Shenandoah heuristic is the policy that decides when a collection cycle starts and how much heap is uncommitted between cycles; the collector offers several, selected with one flag. Acting on that recommendation depends on the JDK build the node runs, since not every build carries the collector, which is why AxonOps also recommends Azul Zulu Builds of OpenJDK. Both recommendations are AxonOps guidance rather than a project position; no upstream source recommends a heuristic at all.

The project has published its own assessment. Its blog post on Java SE 11 support, published 24 February 2022 and covering 4.0.2 and later, calls Shenandoah a low-latency collector that is fully supported in JDK 11 and reports pause targets of roughly 10ms at Cassandra heaps of 12GB, while recommending G1 to operators new to 4.0.


Whether Shenandoah exists in a given JVM is a property of the build, not only of the JDK version. Shenandoah is part of upstream OpenJDK from JDK 12 and was backported to the 11u and 8u update trains, reaching 11u at 11.0.9. On JDK 11 it is a build-time opt-in, so two builds of the same JDK 11 version can differ in whether the collector is there at all. Generational Shenandoah arrived in JDK 25, which no released Cassandra version supports.

Per the OpenJDK Shenandoah wiki, Oracle does not ship Shenandoah in any release, in its OpenJDK builds as well as its proprietary builds. A node on an Oracle build therefore cannot select the collector at any JDK version, and no flag or option file changes that. Builds that carry Shenandoah from 11.0.9 onward include Azul Zulu Builds of OpenJDK, Amazon Corretto and Eclipse Temurin (formerly AdoptOpenJDK).

Confirm the JDK on the node accepts the flag before deploying, since the JVM refuses to start on an unrecognised collector flag:

Terminal window
java -XX:+UseShenandoahGC -version

AxonOps recommends Azul Zulu Builds of OpenJDK for Cassandra. They have carried Shenandoah since OpenJDK 11.0.9, so the collector is available on the JDK 11 that 5.0, 4.1 and 4.0 all support and on the JDK 17 that 5.0 supports. They are licensed under GPLv2 with the Classpath Exception, the same licence as OpenJDK itself, and are free for production and commercial use with no licence fee. The builds that apply to a released Cassandra version are Java 11 and Java 17.

The full name of the build is Azul Zulu Builds of OpenJDK. Match it exactly when obtaining one, rather than searching on the vendor name alone.


Terminal window
# jvm11-server.options or jvm17-server.options
-XX:+UseShenandoahGC
# Keep the resident heap small, leaving host memory to the page cache
-XX:ShenandoahGCHeuristics=compact

Comment out whichever collector block was previously active in that file, G1 on 5.0 or CMS on 4.1 and 4.0. Its settings do not transfer to Shenandoah, so an options file that keeps them records a configuration the node is not running, and -XX:+UseG1GC or -XX:+UseConcMarkSweepGC left in place selects that collector instead.


The compact heuristic runs collection cycles back to back and uncommits unused heap, which keeps the resident heap close to the live set. Cassandra reads depend on the operating system page cache, so memory returned by the heap is memory the kernel can use for SSTable pages.

The cost is throughput. Continuous cycling spends more CPU on collection than adaptive, which starts cycles from the observed allocation rate. Where that CPU cost is measurable in request latency under sustained high allocation rates, adaptive is the fallback.

Shorter pauses cost CPU in every case. AxonOps guidance is that four cases do not repay it:

  • A heap small enough that a G1 collection already finishes in a few milliseconds. There is no pause left for concurrent collection to recover.
  • Single-core hosts, where collection threads and request processing contend for the same core. Cassandra needs several cores in any realistic deployment, so this case is close to hypothetical.
  • Batch-oriented workloads with predictable collection behaviour, where pause timing does not constrain the workload.
  • Deployments in which maximum throughput matters more than latency. Shenandoah spends CPU to hold pauses down, and compact spends more of it than adaptive does.

Two further settings apply where the constraint is the CPU collection uses rather than the length of a pause. Neither has a shipped value to start from, so both are changed against a measurement taken on the node.

  • -XX:ConcGCThreads bounds the threads Shenandoah runs alongside request processing. Reduce it where CPU use during collection stays above 80%, one step at a time. On 5.0.4 and later, cassandra-env.sh sets both GC thread counts to the host core count only when -XX:+UseG1GC is present in the options. Enabling Shenandoah removes that flag with the rest of the G1 block, so the script sets neither count. Both -XX:ConcGCThreads and -XX:ParallelGCThreads therefore keep their JVM defaults unless the options file sets them.
  • -XX:ShenandoahRegionSize fixes the region size in place of the size Shenandoah derives from the heap, which targets 2048 regions within a 256KB to 32MB bound. A larger region lowers the CPU the collector spends, because fewer regions are tracked per cycle. The cost is granularity: collection and uncommit decisions are made a region at a time, so a larger region is a coarser unit for both.

-XX:ShenandoahRegionSize is an experimental JVM option on JDK 11 and JDK 17. -XX:+UnlockExperimentalVMOptions must precede it in the options file; without it the JVM reports the option as experimental and the process does not start. -XX:ShenandoahGCHeuristics and -XX:ConcGCThreads are product options on both JDKs and need no unlock flag.

Terminal window
# jvm11-server.options or jvm17-server.options
# Larger regions, fewer of them to track
-XX:+UnlockExperimentalVMOptions
-XX:ShenandoahRegionSize=32m
# Below the host core count, where collection CPU is the constraint
-XX:ConcGCThreads=4

The computed heap formula still applies under Shenandoah, because on 5.0.4 and later cassandra-env.sh applies the higher of its two caps to any collector other than CMS. MAX_HEAP_SIZE and HEAP_NEWSIZE are subject to the same pairing rule as under CMS, since the check tests for G1 specifically: under Shenandoah, setting MAX_HEAP_SIZE alone stops the node starting. Heap Sizing covers the formula and the two ways to override it.

The soft maximum heap flag is named -XX:ShenandoahSoftMaxHeapSize on JDK 11u (JDK-8253463). Shenandoah honours the generic -XX:SoftMaxHeapSize only from JDK 16, so a setting copied between JDK versions stops the node starting.


The collector reports its own configuration at startup rather than through the process arguments alone. -Xlog:gc+init writes the mode, the heuristic and the region size in use:

Terminal window
# Add -Xlog:gc+init to the options, restart, then inspect the log
grep -E "Mode:|Heuristics:|Heap Region Size:" /var/log/cassandra/gc.log

The heuristic line is the one to check after an edit, since a misspelled value is rejected at startup, whereas a stale G1 or CMS flag left in the file takes effect without any error. The ps command in JVM Tuning reports which collector flag the process received.


  • JVM Tuning - Heap sizing, direct memory, GC logging and the procedure for applying a change
  • G1 - Active on 5.0, and available on 4.1 and 4.0 as a commented block
  • CMS - The collector 4.1 and 4.0 start with
  • ZGC - In no options file on any release, and not recommended by the project
  • JVM Architecture - The pointer-compression boundary, where the heap sits in process memory, and why the heap formula changed
  • GC Pause Issues - Long pauses without an allocation failure