ActiveMQ JVM Memory & GC Tuning: Heap Sizing, G1GC, ZGC Guide

meshIQ July 6, 2026

The JVM is the runtime foundation of every ActiveMQ deployment. Message throughput, delivery latency, producer flow control triggers, OOM crashes, and GC-induced delivery pauses all trace back to JVM memory configuration. Yet ActiveMQ ships with a 512MB heap and no GC logging, appropriate for a developer laptop, not for an enterprise message broker handling millions of messages a day.

This guide covers ActiveMQ JVM memory management and GC tuning from the ground up: where to set JVM flags for Apache ActiveMQ® and Apache Artemis™, the JVM heap memory sizing formula, the fixed-heap rule, GC algorithms selection (G1GC, ZGC, Shenandoah), how GC pauses affect message delivery, the three distinct OOM error types and their specific fixes, KahaDB index cache heap pressure, GC logging configuration, and the container memory alignment required for Kubernetes deployments.

Where to Configure JVM Options

ActiveMQ Apache ActiveMQ®: bin/env

JVM options for Apache ActiveMQ® are set in ACTIVEMQ_OPTS, defined in bin/env (Linux/macOS) or bin/activemq.bat (Windows). This is the correct location, editing the main bin/activemq launch script directly loses changes on upgrade.

ActiveMQ Apache Artemis™: etc/artemis.profile

For Apache Artemis™, JVM options live in the instance’s etc/artemis.profile:

JVM Heap Memory Sizing: The Formula, the Fixed-Heap Rule, and Kubernetes Alignment

The JVM Heap Memory Sizing Formula

JVM heap memory (-Xmx) = 50–60% of available physical RAM

The JVM requires substantially more memory than -Xmx alone accounts for. Off-heap components such as: Metaspace (class metadata), per-thread stack allocations, JIT code cache, JVM internal structures, add 20–30% beyond the heap. 

On a dedicated 8GB broker host, 4–5GB is the appropriate JVM heap memory ceiling. The remaining RAM is used by the OS for file system page caching (which improves KahaDB read performance) and by the JVM’s non-heap overhead.

Host RAMRecommended -XmxRationale
4 GB2 GB2GB for OS, off-heap, KahaDB file cache
8 GB4 GBStandard medium-throughput broker
16 GB8 GBHigh-throughput broker
32 GB16–18 GBEnterprise multi-destination broker
64 GB28–32 GBConsider ZGC at this heap size

The Fixed-Heap Rule: Always -Xms = -Xmx

The single most impactful JVM memory management change for a message broker is fixing the heap size by setting -Xms equal to -Xmx. With a growing heap (e.g., -Xms512m -Xmx4g), the JVM starts small and expands toward the maximum. Each expansion establishes a new memory ceiling, and the GC works harder to stay under it until it gives up and does a full GC to compact the heap before raising the ceiling again.

A Deloitte engineering study of ActiveMQ documented this precisely: the broker running with a small initial and large maximum heap produced full GC events every 20 minutes and exhibited progressively worse throughput consistency as the heap cycled through 1GB → 2GB → 4GB ceilings. Switching to a fixed 1.28GB heap (-Xms1280m -Xmx1280m) eliminated the ceiling-chasing pattern and reduced full GC frequency significantly.

The fixed-heap also enables -XX:+AlwaysPreTouch, which pre-allocates and zero-fills all JVM heap memory pages at startup. Without pre-touching, large GC events trigger OS page faults as the GC first accesses heap memory that hasn’t been physically backed. Pre-touching moves this work to startup (adding seconds), eliminating it from runtime GC events.

Kubernetes: Container Memory Alignment

On Kubernetes, the relationship between JVM heap memory usage and container memory limits is critical. Without -XX:+UseContainerSupport (default in JDK 8u191+, all JDK 9+), the JVM reads the host’s total RAM to calculate ergonomic heap sizes. On a 4GB container running on a 64GB host, the JVM attempts to allocate ~32GB of heap, immediately exceeds the container limit and is OOMKilled by Kubernetes with no application-level error.

We covered the OOMKill failure mode and the container_limit × 0.75 formula in detail in our ActiveMQ on Kubernetes post. The JVM memory sizing and the container memory limit must be explicitly aligned, the JVM does not automatically right-size itself to the container unless UseContainerSupport is active and the container limit is set in the pod spec.

GC Algorithm Selection

G1GC: The Production Default

G1GC (-XX:+UseG1GC) has been the JVM default since Java 9. For message brokers, three properties make it the right baseline choice among available GC algorithms:

  • Compacting collector: Unlike CMS (removed in Java 14), G1GC compacts the heap during collection. This prevents the heap fragmentation that causes CMS to degrade after days or weeks of operation, a critical property for a broker that must run continuously for months.
  • Pause time targeting:XX:MaxGCPauseMillis=20 tells G1GC to aim for GC pauses shorter than 20ms. It is a soft goal as G1GC may occasionally exceed it, but it drives the collector to prefer many small collections over occasional large ones. For message delivery, the difference between a 200ms full GC pause and ten 20ms incremental collections is significant.
  • Short-lived object affinity: ActiveMQ creates enormous volumes of short-lived objects, deserialized frame objects, dispatch table entries, and cursor state that are created and discarded at high rates. G1GC’s Young Generation collection is optimized for this pattern: collect Eden frequently, promote little to Old Gen, and run expensive Old Gen collections rarely.

The NewRatio=4 and InitiatingHeapOccupancyPercent=75 pairing is the key insight from the Deloitte ActiveMQ benchmark. With the default NewRatio=2, too many broker objects (dispatch cursor state, destination metadata) get promoted into Young Gen collections. With NewRatio=4, these long-lived objects stay in Old Gen where they belong, reducing Young Gen GC pressure. With IHOP=75, concurrent marking begins early enough to complete before the JVM heap memory fills, preventing the fallback to stop-the-world full GC.

ZGC: Sub-Millisecond Pause Requirements

ZGC (-XX:+UseZGC) performs concurrent compaction, all live object relocation happens while application threads run. Stop-the-world phases are reduced to safepoint operations measured in microseconds, not milliseconds. ZGC consistently delivers 0.1–0.5ms pause times regardless of JVM heap memory size.

ZGC trade-offs that matter for ActiveMQ:

ZGC’s concurrent GC threads compete with broker dispatch threads for CPU. On a CPU-saturated broker (very high message rate, many concurrent producers/consumers), ZGC’s background threads may not get enough CPU to collect the heap fast enough to keep up with the allocation rate. 

When this happens, ZGC stalls new object allocations, waiting for GC to free JVM memory, producing latency spikes that are different in character from GC pauses but equally disruptive.

ZGC also does not use compressed object pointers, increasing JVM heap memory usage by 15–30% compared to G1GC for the same workload. Size the heap accordingly.

Shenandoah: The OpenJDK Alternative

Shenandoah (-XX:+UseShenandoahGC, OpenJDK 12+) achieves concurrent evacuation, objects are moved while application threads run. Pause times are typically 1-5ms with occasional spikes to 10ms. It is a strong choice for OpenJDK-based deployments where ZGC is not available or appropriate. Generational Shenandoah (production-ready in Java 25) improves throughput significantly.

GC Algorithm Decision Matrix

AlgorithmJDK RequiredTypical PauseCPU OverheadBest For ActiveMQ
G1GCJava 9+ (default)10–200ms tunableLowMost production deployments start here
ZGCJava 15+ (prod)0.1–1ms+5–10% CPUSLA-gated latency requirements, large heaps
ShenandoahOpenJDK 12+1–10ms+5–15% CPUOpenJDK deployments needing low latency
CMSRemoved Java 14Never, deprecated, and removed
Parallel GCAllHigh varianceLowNever for a broker

GC Pauses and Message Delivery

Every stop-the-world GC event halts all JVM threads, including every broker dispatch thread, every network I/O thread, and every KahaDB persistence thread. During a GC pause:

  • Messages queued for dispatch are not delivered
  • Consumer acknowledgments in transit are stalled, preventing message removal from queues
  • Producer flow control signals cannot be evaluated, potentially delaying PFC unblock even after space is freed
  • KahaDB checkpoint operations miss their scheduled window

For a 200ms full GC pause on a broker processing 10,000 messages per second, approximately 2,000 messages are delayed by 200ms. After the pause, the dispatch backlog is flushed in a burst, which can temporarily exceed consumer processing capacity and stress the consumer side.

Making GC pauses smaller and less frequent:

  • Fix JVM heap memory size (-Xms = -Xmx) to eliminate growth-phase full GCs
  • Set InitiatingHeapOccupancyPercent=75 to trigger concurrent marking before the heap fills, preventing stop-the-world fallback
  • Tune NewRatio=4 to right-size the Old Gen for the broker’s long-lived object population
  • Monitor full GC frequency with GC logging, a full GC more than once per hour under normal load is a GC tuning signal

Three OOM Error Types and Their Specific Fixes

The exact OOM error message in the broker log identifies which type you are experiencing. Each type has a different root cause in JVM memory management.

Type 1: OutOfMemoryError: Java heap space

The JVM heap memory is exhausted. Three distinct root causes:

a) Undersized -Xmx: Increase JVM heap memory using the formula above. Profile under load with JConsole or Java Mission Control to see actual JVM heap memory usage under production message volumes.

b) Slow consumer message accumulation: Messages from lagged consumers fill broker memory faster than they are consumed. The fix is not purely to increase heap, it is to address the slow consumer (covered in our Slow Consumer Detection & Handling and ensure memoryUsage percentOfJvmHeap is configured to trigger producer flow control before OOM occurs.

c) KahaDB PageFile LRUCache filling the heap (the most specific and least-documented cause): The KahaDB persistence layer maintains an LRU cache of index page file entries. By default, this cache holds 10,000 entries, and on brokers with many destinations and large persistent message volumes, this cache alone can consume several hundred megabytes of JVM heap memory. 

A documented Red Hat support case confirms this scenario: heap dump analysis showed a KahaDBStore instance with its internal PageFile containing a 10,000-entry LRUCache retaining most of the available heap.

The fix is to reduce indexCacheSize in activemq.xml:

Type 2: OutOfMemoryError: GC overhead limit exceeded

The JVM has spent more than 98% of CPU time on GC but recovered less than 2% of heap per cycle, a sign that JVM memory management has broken down entirely. The heap is effectively full, and the GC is running continuously without meaningful progress.

Root causes:

(1) the heap is permanently at or near capacity because message accumulation rate exceeds the GC’s ability to reclaim space;

(2) producer flow control is disabled (producerFlowControl=”false” on destination policy), removing the backpressure that would otherwise prevent heap saturation;

(3) a memory leak in a broker plugin or custom interceptor gradually fills Old Gen until full GC cannot reclaim space.

The fix is not simply to increase -Xmx indefinitely, at high enough JVM memory pressure, the heap will fill again faster than GC can help. The correct fix is to identify what is holding heap references (profile with Java Flight Recorder), ensure producer flow control is enabled, and address the root consumption rate problem.

Type 3: OutOfMemoryError: unable to create native thread

This is not a JVM heap memory problem, the heap may be nearly empty. The OS cannot create additional threads for the JVM, either because the process has reached the OS thread limit (ulimit -u) or because virtual address space for thread stacks is exhausted.

The ActiveMQ-specific root cause: by default, Apache ActiveMQ® creates a dedicated thread per destination (-Dorg.apache.activemq.UseDedicatedTaskRunner=true). On a broker with 500 queues and 500 topics, this creates 1,000 threads. Each thread stack requires 512KB–1MB of native memory (from -Xss). On many Linux servers, the default ulimit -u per process is 4,096 threads.

Fix:

This is the recommended configuration for any broker with more than ~50 destinations. The performance impact is minimal, ActiveMQ’s dispatch logic is already largely asynchronous and does not depend on per-destination threads for correctness.

GC Logging: The Non-Negotiable Production Setting

Running ActiveMQ without GC logging means having no diagnostic data during a GC-caused incident. GC logs answer questions after an incident: how often were full GCs occurring before the OOM crash, how long were the pause events that coincided with the latency spike, and was the JVM heap memory usage growing toward saturation over the previous 24 hours?

This configuration logs all GC events at appropriate detail levels, writes to a rolling file (5 × 20MB = 100MB of history retained), timestamps each event with both wall clock time and JVM uptime, and rotates automatically. On a healthy broker, GC log growth is typically 2–5MB per day.

Analyzing GC logs:

  • GCeasy (gceasy.io): upload the log for instant analysis, pause distributions, heap memory utilization, promotion rates, and full GC frequency
  • Java Mission Control (JMC): connects live or analyzes JFR recordings with full JVM profiling
  • Java Flight Recorder (JFR): continuous low-overhead production profiling that captures GC events, thread activity, and JIT compilation together

Key metrics to review in GC logs:

  • Young GC pause: should be <20ms with MaxGCPauseMillis=20
  • Full GC frequency: more than once per hour under normal load is a GC tuning signal
  • Old Gen occupancy after major GC: >70% after collection means the JVM heap memory needs to grow or the Old Gen population needs to shrink

Complete Production JVM Configuration Reference

The following is a production-validated configuration for Apache ActiveMQ® on a dedicated 8GB host:

For a latency-sensitive deployment requiring sub-millisecond GC pauses (JDK 15+):

ActiveMQ JVM Tuning Review for Your Production Environment JVM memory

configuration errors are silent until catastrophic – OOM crashes, multi-second GC pauses, and Kubernetes OOMKill events with no application-level errors. meshIQ’s team has tuned ActiveMQ JVM memory management configurations across hundreds of enterprise deployments and can review your current settings against your workload profile and OOM history.

Request a JVM Review

JVM Tuning Checklist for ActiveMQ Production

Heap Sizing:

  • -Xms = -Xmx (fixed JVM heap memory – no growth-phase GC events)
  • -Xmx ≤ 50–60% of physical RAM (bare metal) or 70–75% of container limit (K8s)
  • -XX:+UseContainerSupport enabled (for K8s/Docker – default in JDK 8u191+)

GC Configuration:

  • -XX:+UseG1GC (or -XX:+UseZGC for sub-millisecond requirements on JDK 15+)
  • -XX:MaxGCPauseMillis=20 (G1GC pause target — core GC tuning lever)
  • -XX:InitiatingHeapOccupancyPercent=75 (deferred concurrent marking — Deloitte-validated)
  • -XX:NewRatio=4 (right-size Old Gen for long-lived broker objects)
  • -XX:+UseStringDeduplication (G1GC — reduce JVM memory footprint from message header duplication)
  • -XX:+AlwaysPreTouch (eliminate runtime page faults from GC)
  • -XX:MaxMetaspaceSize=512m (cap Metaspace growth)

Thread Management:

  • -Dorg.apache.activemq.UseDedicatedTaskRunner=false (thread pool for many destinations)

Observability:

  • GC logging enabled: -Xlog:gc*:file=… with rolling files
  • -XX:+HeapDumpOnOutOfMemoryError + -XX:HeapDumpPath configured
  • -XX:OnOutOfMemoryError=’kill -9 %p’ (let supervisor restart the process)

Configuration Alignment:

  • activemq.xml memoryUsage percentOfJvmHeap=”70″ consistent with JVM heap memory size
  • kahaDB indexCacheSize reviewed if OOM heap dumps show LRUCache dominance

JVM Memory Configuration Is Infrastructure

The ACTIVEMQ_OPTS file is as important as activemq.xml. The JVM memory management configuration in this guide is a production-validated starting point, profile under your actual workload with GCeasy or Java Mission Control, monitor full GC frequency in the first weeks of production, and adjust GC tuning parameters like InitiatingHeapOccupancyPercent and NewRatio based on your observed Old Gen occupancy patterns.

Get your ActiveMQ JVM configuration reviewed → Request a JVM Review

Frequently Asked Questions

Q1: What is the recommended JVM heap memory size for ActiveMQ? 

Start with 2–4GB JVM heap memory for typical production workloads, not exceeding 50–60% of available physical RAM. For Kubernetes, set -Xmx to 70–75% of the container memory limit. Always set -Xms = -Xmx for fixed JVM heap memory, this is the single most impactful JVM memory management change you can make.

Q2: Which GC algorithm is best for ActiveMQ? 

Among the available GC algorithms, G1GC is the right choice for most deployments, the JVM default since Java 9, compacting, and pause-time tunable. ZGC (JDK 15+) for sub-millisecond pause requirements. Never use CMS (removed in Java 14) or Parallel GC for a message broker. Start with G1GC GC tuning before considering ZGC.

Q3: How do I set JVM options for ActiveMQ? 

Apache ActiveMQ®: ACTIVEMQ_OPTS in bin/env. Apache Artemis™: JAVA_ARGS in etc/artemis.profile. Never edit the main launch scripts directly, edits are lost on upgrade.

Q4: What causes OutOfMemoryError in ActiveMQ? 

Three distinct JVM memory types with different fixes: (1) Java heap space, undersized JVM heap memory, slow consumer accumulation, or KahaDB indexCacheSize LRUCache; (2) GC overhead limit exceeded, JVM heap memory usage permanently full, a GC tuning failure; (3) unable to create native thread, thread exhaustion from UseDedicatedTaskRunner=true with many destinations.

Q: How does GC affect ActiveMQ message delivery? 

Stop-the-world GC events halt all broker threads, including dispatch. A 200ms full GC = 200ms delivery silence. The fix combines proper JVM heap memory sizing, MaxGCPauseMillis=20 GC tuning, and InitiatingHeapOccupancyPercent=75. For stricter requirements, switch GC algorithms from G1GC to ZGC.

Cookies preferences

Others

Other uncategorized cookies are those that are being analyzed and have not been classified into a category as yet.

Necessary

Necessary
Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously.

Advertisement

Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.

Analytics

Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.

Functional

Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.

Performance

Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.