Skip to content

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

CMS for Cassandra

The Concurrent Mark Sweep collector (CMS) marks the old generation concurrently with application threads, pausing only for an initial mark and a remark phase. It sweeps reclaimed space in place rather than compacting it, so a long-lived old generation fragments, and an allocation that the fragmented space cannot satisfy forces a full stop-the-world compacting collection.

CMS is what Cassandra 4.1 and 4.0 start with. Its block is active in jvm11-server.options, which is identical on both releases. That is the state of a default install rather than the only collector those releases support: the same file carries a full G1 block, commented out and tuned by the project, and G1 is present in every JDK build 4.1 and 4.0 support. Neither release confines an operator to CMS, and Choosing Between CMS and G1 gives the cases in which G1 is the better choice on them.

CMS is not available on 5.0 in the supported configuration. JDK 14 removed the collector (JEP 363), so jvm17-server.options on 5.0 carries no CMS block, and a node running 5.0 under JDK 17 cannot select it. jvm11-server.options on 5.0 still carries the block, commented out, so an operator running 5.0 under JDK 11 may enable it by hand. G1 is active on a default install of every 5.0 patch version.


These are the values in jvm11-server.options on a default install. They are already active; adding them again changes nothing. Selecting G1 means commenting out every one of them: -XX:+UseConcMarkSweepGC left active keeps the node on CMS, and -XX:MaxTenuringThreshold left active continues to apply under G1.

Setting4.1 and 4.0Effect
-XX:+UseConcMarkSweepGCactiveSelects CMS. The JVM prints a deprecation warning at start under JDK 11 (JEP 291) and runs the collector.
-XX:CMSInitiatingOccupancyFraction75Old-generation occupancy, as a percentage, at which a concurrent cycle starts.
-XX:+UseCMSInitiatingOccupancyOnlyactiveHolds the trigger at that occupancy instead of letting CMS derive it from its own statistics.
-XX:MaxTenuringThreshold1Survivor copies an object takes before promotion to the old generation.
-XX:SurvivorRatio8Eden size relative to one survivor space, so each survivor space is a tenth of the young generation.
-XX:+CMSParallelInitialMarkEnabledactiveInitial mark runs on multiple threads, shortening that stop-the-world phase.
-XX:+CMSParallelRemarkEnabledactiveRemark runs on multiple threads, shortening the longer of the two stop-the-world phases.
-XX:+CMSEdenChunksRecordAlwaysactiveEden chunk boundaries are recorded as allocation happens rather than sampled when a young collection begins, which spreads eden scanning evenly across the collection threads.
-XX:CMSWaitDuration10000Milliseconds the CMS thread waits for a young collection before starting the initial mark without one. Marking straight after a young collection leaves less live data in eden for the initial mark to scan.
-XX:+CMSClassUnloadingEnabledactiveClasses are unloaded during the concurrent cycle rather than only during a full collection. The shipped comment cites CASSANDRA-6541: some JVMs fill their heap when accessed over JMX.

-XX:ParallelGCThreads and -XX:ConcGCThreads are set by neither the options file nor cassandra-env.sh on 4.1 and 4.0, so the JVM derives both from the processors available to the process.

-XX:-UseBiasedLocking is a general setting rather than part of the CMS block, active in jvm-server.options on both releases; see What the Release Already Sets.


The collection trigger is fixed rather than adaptive. -XX:CMSInitiatingOccupancyFraction=75 with -XX:+UseCMSInitiatingOccupancyOnly starts a concurrent cycle at 75% old-generation occupancy for the life of the process, and the second flag is what stops CMS from choosing that point itself. This is the contrast with G1 on 5.0, where -XX:InitiatingHeapOccupancyPercent is a starting value that adaptive sizing replaces once G1 can predict its own threshold. On 4.1 and 4.0 the value in the options file is the value in use.

The failure the trigger guards against is a concurrent mode failure: the old generation fills before the concurrent cycle completes, and the JVM falls back to a stop-the-world compacting collection whose duration scales with the live set rather than with any goal. The GC log records it as concurrent mode failure. A related entry, promotion failed, appears where a young collection cannot promote into a fragmented old generation.

CMS has no pause-time goal, so there is no equivalent of -XX:MaxGCPauseMillis to set. The bound that matters is the request timeout, read_request_timeout and write_request_timeout in cassandra.yaml (read_request_timeout_in_ms and write_request_timeout_in_ms on 4.0): a collection longer than the timeout produces failed requests whatever the collector was configured with. The comparison for a node running CMS is therefore against the pause baseline the node itself establishes under representative load.


Where concurrent mode failure or promotion failed appears under representative load, lower the fraction in steps of 5 and re-measure. Each step starts cycles earlier, which leaves more headroom for allocation during the cycle and spends more CPU on collection while the node serves requests. Raising the fraction above 75 has the opposite effect on both.

The shipped survivor configuration promotes early. With -XX:MaxTenuringThreshold=1, an object that survives a young collection is promoted rather than aged further in the survivor spaces, and -XX:SurvivorRatio=8 keeps those spaces small. Where the GC log shows old-generation occupancy rising between cycles on a workload whose objects are short-lived, raising the tenuring threshold keeps more of that data in the young generation, and lowering the survivor ratio gives it somewhere to sit at the expense of eden. Both changes cost copying work in each young collection, so change one at a time and re-measure.

Terminal window
# jvm11-server.options on 4.1 and 4.0
# 1. Start the concurrent cycle earlier, where the GC log shows concurrent mode failure
-XX:CMSInitiatingOccupancyFraction=70
# 2. Keep short-lived objects out of the old generation, where promotion is the problem
-XX:MaxTenuringThreshold=2
-XX:SurvivorRatio=4

The young generation is a fixed size under CMS, set with -Xmn in jvm-server.options or HEAP_NEWSIZE in the environment. Neither is valid under G1.

MAX_HEAP_SIZE and HEAP_NEWSIZE must be set as a pair on 4.1 and 4.0. Setting one alone stops the node starting, with a message asking for both. The options-file flags are not symmetric with the environment variables: -Xmn without -Xmx stops the start, while -Xmx on its own starts, because cassandra-env.sh then computes the young generation itself and appends -Xmn. Changing the Heap covers both mechanisms and which one wins where both are present.

cassandra-env.sh on 4.1 and 4.0 computes the same max heap under either collector, so switching to G1 changes the young generation handling above but not the heap. On 5.0.4 and later, where CMS has been enabled by hand under JDK 11, the script applies a lower cap under CMS than under any other collector. Heap Sizing gives both caps and the computed value for each host size and release.


The options file states intent. What the JVM started with is on the command line of the process, which also carries the -Xmn that cassandra-env.sh computes and appends under CMS:

Terminal window
ps -o args= -p "$(pgrep -fn CassandraDaemon)" | tr ' ' '\n' \
| grep -E 'UseConcMarkSweepGC|CMSInitiatingOccupancyFraction|MaxTenuringThreshold|SurvivorRatio|^-Xmn'

-XX:+UseConcMarkSweepGC in the output confirms the collector, and its absence on 4.1 or 4.0 means the node has been switched to another one by hand.


  • 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
  • Shenandoah - The collector AxonOps recommends over G1, in no options file on any release
  • 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