Zimbra Mailboxd Java Tuning For Large Environments

Zimbra Mailboxd Java Tuning For Large Environments

The heart of Zimbra is mailboxd, a big Java application. Zimbra ships with Java, and Java Garbage Collection (“GC”) settings tuned with defaults that are suboptimal for Zimbra mailbox servers supporting demanding environments: large mailboxes (up to 200GB per user), users who regularly send and receive large attachments (up to 50MB), and users who access mail simultaneously across multiple clients — web client, ActiveSync mobile devices, and IMAP.

In such environments, the default Java Virtual Machine (“JVM”) settings that ship with Zimbra are often insufficient, leading to performance issues, excessive swapping, and (sometimes) mailboxd crashes.

This guide documents what can go wrong, the key JVM parameters to tune, explains why each one matters, and provides practical rules of thumb and diagnostic tests to help you find the right values for your specific server(s).


Java and Java Garbage Collection Intro for Newbies

Java is a “write once; run anywhere” programming language. Memory management, discarded object management, and other housekeeping functions developers normally need to worry about when programming in other languages are handled by Java itself. Here’s how it all works.

The Java Heap

When Zimbra’s mailbox server service (mailboxd) starts up, Java reserves a large block of memory called the heap. This is the workspace where all of mailboxd’s in-memory data lives: email messages being processed, user session state, attachment buffers, and so on. You control how large this workspace is with the localconfig attribute mailboxd_java_heap_size. Like your cubicle at work, periodically you have to tidy up. With a Java application, tidying up is the job of the Java Garbage Collector.

Java Garbage Collection

Java programs create objects constantly — a data structure to hold an incoming email here, a buffer to parse an attachment there. Many of these objects are only needed briefly. The garbage collector (GC) is the part of the JVM responsible for periodically finding objects that are no longer needed and reclaiming their memory so it can be reused. Without GC, the heap would fill up and the server would crash within just a few minutes.

Often, the GC can do the tidying up of the heap while Zimbra is running. But sometimes, the GC has to stop all Java application threads briefly — called a Stop-The-World (“STW”) pause — to safely examine what’s live and what’s not. These pauses are measured in milliseconds. Short pauses are invisible to users. Long ones (hundreds of milliseconds or more) cause sluggish webmail, IMAP timeouts, and failed ActiveSync syncs.

Further, GC requires CPU cycles. It runs typically every few seconds. But if the server is really busy, and GC is tuned suboptimally, you can run out of CPU cycles.

The goal of GC tuning is to keep pauses short and frequent rather than rare and long, and to prevent the garbage collector from ever getting into a situation where it can’t find enough free space to do its job.

GC gets its own log file, /opt/zimbra/log/gc.log, owned by root, so we can grep through this to get the detailed info we need. But Zimbra also logs more summary GC events to /opt/zimbra/log/zmmailboxd.out, which we primarily use to identify issues.

What Is G1GC?

The Garbage First Garbage Collector (G1GC, or just G1) is the GC algorithm Zimbra uses by default. G1 divides the entire heap into a large number of equal-sized chunks called regions — typically 2048 of them. Rather than treating the whole heap as one big space, G1 manages these regions independently. This lets it be smarter about which parts of memory to clean up first (it prioritizes the regions with the most garbage — hence “Garbage First”) and allows it to do much of its work concurrently in the background while Zimbra continues serving users, avoiding Stop-The-World pauses.

What Is the Young Generation?

G1 classifies regions into two broad categories based on how old the objects inside them are.

The Young Generation (young gen) consists of regions that hold recently-created objects — things that were just allocated. Most of these objects are very short-lived. A buffer created to parse an email attachment might only be needed for a fraction of a second. Young gen GC (called a Minor GC or Evacuation Pause) runs frequently and quickly, reclaiming all those short-lived objects. The surviving objects — the ones still in use — get copied (“evacuated”) out of young gen regions into longer-term storage.

The young gen is further divided into Eden (where new objects are born) and Survivor spaces (where objects that survived at least one GC cycle wait before being promoted to old gen). You don’t need to manage these directly — G1 handles the subdivision automatically.

The key thing to understand: young gen GC requires To-space — free regions to copy live objects into. If there aren’t enough free regions available when young GC runs, you get a To-space exhausted event, which is a serious failure mode that causes long pauses and heap fragmentation.

What Is the Old Generation?

The Old Generation (old gen) holds objects that have survived multiple young gen GC cycles — things that are clearly long-lived. In Zimbra, old gen typically contains: cached mailbox metadata, long-running IMAP session state, connection pool objects, and similar persistent structures.

Old gen grows over time as objects are promoted from young gen. It is reclaimed by Mixed GC cycles, which G1 runs after completing a concurrent marking cycle. If old gen fills up faster than it can be reclaimed, the heap fills up, To-space runs out, and eventually you get an Out of Memory error or a full Stop-The-World collection that can pause the server for seconds.

Old gen growth in your GC logs is visible as the post-GC heap size gradually increasing over several days. In GC log lines like 4800M->1600M(7896M), the 1600M is the heap size after GC completes — and this number is dominated by live old gen objects. If this number climbs steadily over hours without ever coming back down, old gen is accumulating faster than GC is reclaiming it.

What Are Humongous Allocations?

G1 has a special problem with very large objects. If a single object is larger than half the size of one region, G1 can’t fit it into a normal region — it instead allocates it directly into a set of contiguous regions in old gen. This is called a humongous allocation, and the object is called a humongous object.

Humongous objects are problematic for several reasons. They bypass young gen entirely, so they go straight into old gen and stay there until the next marking cycle identifies them as dead. They consume whole regions even if they don’t fill them, wasting space. And each humongous allocation forces G1 to check whether a concurrent marking cycle needs to start, adding overhead. In large Zimbra environments, humongous allocations are common because large email attachments, IMAP session buffers, and search index chunks can easily exceed the default 2 MB humongous threshold.

In zmmailboxd.out, humongous allocation events look like this:

GC(n) Pause Young (Concurrent Start) (G1 Humongous Allocation) 6200M->1000M(7896M) 38ms
GC(n+1) Concurrent Undo Cycle
GC(n+1) Concurrent Undo Cycle 6ms

The Concurrent Undo Cycle immediately after is G1 realising there was nothing to reclaim and rolling back — pure wasted effort.

What Is IHOP (Initiating Heap Occupancy Percent)?

Before G1 can reclaim old gen memory, it needs to run a concurrent marking cycle — a background process that traces through all live objects in the heap and marks which old gen regions contain mostly garbage (and are therefore good candidates for reclamation). This happens concurrently, meaning Zimbra keeps running while it happens, but it does consume CPU.

IHOP controls when G1 decides to start this background marking cycle. Specifically, it’s the heap occupancy percentage at which G1 says “I should start marking now so I have time to finish before the heap gets full.” The default is 45%, meaning G1 waits until 45% of the heap is in use before starting.

But our experience has been that in a busy Zimbra environment with fast allocation rates, 45% can be too late. By the time the concurrent marking cycle completes and mixed GC cycles start reclaiming old gen, the heap may have already climbed to 85–95% full. Lowering IHOP to 35% gives G1 a head start, allowing it to finish marking and begin freeing old gen before the heap gets dangerously full.

What Is To-Space Exhaustion?

When G1 runs a young gen GC, it needs to copy all the live objects out of young gen regions into free regions — these free regions are called To-space. If G1 can’t find enough free regions to complete this copying (because old gen has grown to fill most of the heap), it declares To-space exhausted.

This is one of the most disruptive things that can happen in G1. Objects that couldn’t be evacuated get left in place and pinned, the GC has to do extra recovery work, and the resulting pauses can be hundreds of milliseconds long. Users experience To-space exhaustion as sudden server sluggishness, IMAP disconnects, or ActiveSync sync errors.

To-space exhaustion appears in zmmailboxd.out as:

GC(n) To-space exhausted
GC(n) Pause Young (Prepare Mixed) (G1 Preventive Collection) 7788M->1491M(7892M) 50ms

Preventing To-space exhaustion is the primary goal of most of the tuning steps in this guide.  It’s not just Zimbra that has been facing this issue either!

What Is G1ReservePercent?

Because To-space exhaustion is so disruptive, G1 has a built-in safety mechanism: G1ReservePercent. This parameter tells G1 to always hold a certain percentage of the heap in reserve — kept empty and available specifically as emergency To-space. Think of it as a buffer zone that G1 refuses to fill with objects, so that when a young gen GC needs to evacuate live objects, there are always free regions waiting.

The default reserve is 10% of the heap — confirmed by Oracle’s Java documentation and verified on production Zimbra servers by inspecting the running JVM command line via /proc/<pid>/cmdline, which shows no explicit G1ReservePercent setting, confirming the JVM default is in effect. On an 8 GB heap, that’s about 800 MB held back at all times.

Raising G1ReservePercent to 20 gives G1 a larger safety net. The trade-off is that the reserved space isn’t available for normal object allocation, so your effective heap is slightly smaller — but giving up an extra 800 MB as reserve to eliminate To-space exhaustion events is almost always the right decision.

G1ReservePercent is best thought of as your last line of defence against To-space exhaustion. The other tuning steps in this guide reduce the likelihood of running out of To-space. G1ReservePercent ensures that even if those measures are imperfect, there’s always a reserve to fall back on.

What Is a Concurrent Mark Cycle?

A concurrent mark cycle is G1’s background process for figuring out which old gen objects are still alive and which can be freed. It runs in parallel with the application — Zimbra keeps handling email while this happens — so it’s relatively low-impact compared to a full Stop-The-World collection. It consists of several phases: initial mark (brief STW), concurrent marking (background), remark (brief STW), and cleanup (brief STW).

Once a concurrent mark cycle completes, G1 has a map of which old gen regions are mostly garbage. It then schedules Mixed GC cycles that collect both young gen and selected old gen regions, actually reclaiming that old gen memory. Without regular concurrent mark cycles completing, old gen just grows until the heap is full.

In zmmailboxd.out, a healthy concurrent mark cycle looks like:

GC(n) Concurrent Mark Cycle
GC(n) Pause Remark 2100M->1950M(7896M) 45ms
GC(n) Pause Cleanup 1950M->1950M(7896M) 0.2ms
GC(n) Concurrent Mark Cycle 2800ms

Putting the Concepts Together

Here’s how everything fits together in a running Zimbra server:

  1. Zimbra constantly creates new objects (email processing, user sessions, attachment buffers). These go into young gen.
  2. Periodically, a young gen GC runs, reclaiming short-lived objects and promoting survivors to old gen. This requires To-space (free regions). G1 always keeps a reserve of free regions for this purpose, controlled by G1ReservePercent.
  3. Old gen gradually fills up with long-lived objects. When it reaches the IHOP threshold, G1 starts a concurrent mark cycle in the background.
  4. After marking completes, mixed GC cycles reclaim old gen regions that are mostly garbage, freeing space.
  5. If old gen fills up faster than marking and mixed GC can reclaim it — or if humongous allocations fragment the heap — To-space runs out, causing To-space exhausted events and long pauses.

The tuning steps in this guide address each point in this chain: sizing the heap correctly, establishing a To-space reserve, controlling young gen size, raising the humongous threshold, and starting concurrent marking at the right time.  


Background: How Zimbra Uses the JVM

Zimbra’s mailbox server (mailboxd) runs inside a Java Virtual Machine. All in-flight mail processing, IMAP session state, ActiveSync device sync buffers, message parsing, attachment handling, and search indexing happen inside this JVM’s heap. The garbage collector (GC) is responsible for reclaiming memory from objects that are no longer needed.

Zimbra ships with the G1 garbage collector (-XX:+UseG1GC) which has several tuning knobs that dramatically affect behaviour under the kind of workload described above. Large attachments in particular create objects that G1 treats specially, and multiple concurrent client sessions create sustained allocation pressure that can overwhelm default settings.

The JVM options for mailboxd are controlled by two zmlocalconfig keys:

  • mailboxd_java_options — JVM flags (GC settings, logging, etc.)
  • mailboxd_java_heap_size — heap size in MB (sets both -Xms and -Xmx)

Changes to these keys are preserved across Zimbra patches (but not Zimbra major version upgrades in our experience), and take effect after a mailboxd restart.


Step 1: Confirm Your Current Settings

Before changing anything, baseline your current configuration:

# As the zimbra user:
zmlocalconfig mailboxd_java_options
zmlocalconfig mailboxd_java_heap_size

# Confirm what the running JVM is actually using:
ps aux | grep java | grep mailboxd | grep -oP '\-Xm[sx][^ ]+'

# Check actual heap region size from the GC log (run as root):
head -20 /opt/zimbra/log/gc.log | grep "Heap "

The last command is important — it will show you lines like:

Heap Region Size: 4M
Heap Min Capacity: 7892M
Heap Initial Capacity: 7892M
Heap Max Capacity: 7892M

Note that Heap Min Capacity and Heap Max Capacity should match.


Step 2: Set the Heap Size Correctly

Rule of Thumb

For mailbox servers in large environments:

System RAM Recommended mailboxd_java_heap_size
16 GB 4096–6144 MB
32 GB 6144–8192 MB
64 GB 8192–12288 MB

 

Important caveat from Zimbra’s own Performance Tuning Guidelines wiki (very dated now, but in some places like this, still accurate): Do not allocate more than ~30% of system RAM to the Java heap. The operating system, MariaDB (which Zimbra also relies on heavily), and other processes all need memory too. Swapping is catastrophically bad for Zimbra performance.

And:

…beyond a heap size of approximately 6.4 GB, the benefit to mailboxd often diminishes. Additional RAM beyond that point is frequently better allocated to the MariaDB innodb_buffer_pool_size. Check ~/conf/my.cnf and consider whether your database buffer pool is undersized before increasing the Java heap further.

Test: Is Your Heap Too Small? Do You Need JVM Tuning?

Look for these patterns in /opt/zimbra/log/zmmailboxd.out:

GC(n) To-space exhausted
GC(n) Pause Young ... 7800M->1500M(7892M) 185ms

If you see To-space exhausted events, or if the heap is regularly reaching 95%+ occupancy before GC fires (the first number approaching the total in parentheses), your heap may be undersized — or your GC tuning needs adjustment (read on before simply increasing heap size).

If Zimbra has been running for ~four days or more, you don’t see any To-space exhausted errors, your Java heap size is within the recommendations above, and users are not complaining of system slowness, you might be fine as-is.  If you want to build in some headroom, carry on!


Step 3: Set the G1 Reserve Percent

The Parameter

-XX:G1ReservePercent=20

As explained above, G1 maintains a reserve of free heap regions specifically to serve as emergency To-space during young gen GC evacuations. The default is 10% — confirmed by Oracle’s documentation and verified on production Zimbra servers by inspecting the running JVM command line via /proc/<pid>/cmdline, which shows no explicit G1ReservePercent setting, confirming the JVM default is in effect.

This is the most direct lever for preventing To-space exhaustion, and it belongs early in the tuning process because it provides an immediate safety net while you work through the remaining steps. The other steps reduce the causes of To-space pressure; G1ReservePercent addresses the consequence directly.

Rule of Thumb

Setting When to use
10% (default) Adequate only if all other tuning steps are applied and load is moderate
15% A sensible middle ground for most large Zimbra environments
20% Recommended if you have experienced To-space exhaustion events, or have high concurrent client counts

On an 8 GB heap, raising from 10% to 20% costs you roughly 800 MB of usable heap. This is almost always worth it — the cost of a To-space exhaustion event (long pauses, potential client timeouts, fragmented heap) far exceeds the cost of a slightly smaller effective heap. Do not set this above 25% — reserving too much of the heap means less space for actual objects, which will cause more frequent GC cycles without any benefit.

Test: Is Your Reserve Being Maintained?

After applying this setting, you should no longer see To-space exhausted in zmmailboxd.out. If you still do, it means even the expanded reserve is being consumed — which points to a combination problem: old gen growing too fast (lower IHOP), young gen too large (lower G1MaxNewSizePercent), or humongous allocations too frequent (raise G1HeapRegionSize). Apply those steps first, then use G1ReservePercent as the final safety net.


Step 4: Tune G1 Young Generation Size

The Parameters

-XX:G1NewSizePercent=10
-XX:G1MaxNewSizePercent=30

These control the minimum and maximum size of the young generation as a percentage of total heap. The Zimbra defaults (15% min, 45% max) allow the young gen to grow very large. On a large heap under heavy allocation, a large young gen means more objects to evacuate per GC cycle, which requires more free To-space regions. If old gen has grown to fill most of the heap, there may not be enough free regions available — leading to To-space exhaustion.

Rule of Thumb

  • G1NewSizePercent: Set to 10. This gives young gen a reasonable floor without over-constraining it.
  • G1MaxNewSizePercent: Set to 30. This caps young gen at roughly 2.4 GB on an 8 GB heap, leaving plenty of free regions for To-space during evacuation.

The Trade-off

Reducing the max young gen size means GC runs more frequently, but each pause is shorter because there’s less data to evacuate. For a mail server, many short pauses (10–40 ms) are far preferable to occasional long stalls (500 ms–2 s) during To-space exhaustion recovery. Users experience the latter as sluggish webmail, IMAP timeouts, and ActiveSync sync failures.

Test: Are Young Gen Pauses Acceptable?

In your GC log, look at the pause times on Pause Young lines:

GC(n) Pause Young (Normal) (G1 Evacuation Pause) 4800M->1600M(7896M) 34ms

The last value is the pause duration. For a mail server:

  • Under 50 ms: Excellent — users will not notice
  • 50–150 ms: Acceptable for most workloads
  • 150–500 ms: Noticeable; worth investigating
  • Over 500 ms: Will cause user-visible delays and possible client timeouts

Step 5: Increase the G1 Heap Region Size

The Parameter

-XX:G1HeapRegionSize=8m

G1 divides the heap into equal-sized regions. The default region size is calculated as heap_size / 2048, rounded to the nearest power of 2 (valid values: 1, 2, 4, 8, 16, 32 MB). On a ~8 GB heap, the default is 4 MB.

This matters because any single object larger than half a region size is treated as a humongous object — it bypasses the young generation entirely, is allocated directly into old gen, consumes whole regions, and causes G1 to trigger a concurrent GC cycle to check whether the allocation is safe. In a default 4 MB region configuration, any object larger than 2 MB is humongous.

In large Zimbra environments, humongous objects appear frequently:

  • Large email attachments being parsed or buffered (50 MB attachments easily produce large intermediate objects)
  • IMAP session state for users with large mailboxes
  • ActiveSync sync buffers for multiple simultaneous device syncs
  • Lucene/Solr index segments during search operations

Frequent humongous allocations fragment the old generation, reduce the number of free regions available for To-space, and cause the GC log to fill with patterns like this:

GC(n) Pause Young (Concurrent Start) (G1 Humongous Allocation) 6200M->1000M(7896M) 38ms
GC(n+1) Concurrent Undo Cycle
GC(n+1) Concurrent Undo Cycle 6ms

The Concurrent Undo Cycle immediately following a humongous allocation trigger is G1 realising there was nothing to reclaim and rolling back — wasted work.

Rule of Thumb

Heap Size Default Region Size Recommended Region Size Humongous Threshold
~4 GB 2 MB 4 MB 2 MB
~8 GB 4 MB 8 MB 4 MB
~16 GB 8 MB 8–16 MB 4–8 MB

For most large Zimbra environments on an 8 GB heap, G1HeapRegionSize=8m is the right choice. Zimbra does not set this by default, so you will have 4m regions if not set explicitly in mailboxd_java_options. If you’re still seeing humongous allocations after this change, consider 16m, but be aware that larger regions mean fewer total regions, which reduces G1’s flexibility in choosing what to collect.

Confirm the Change Took Effect

After restarting mailboxd (as root):

head -10 /opt/zimbra/log/gc.log | grep -i region

You should see Heap Region Size: 8M. If you still see 4M, the parameter didn’t apply — double-check your mailboxd_java_options string for typos.

Test: Are Humongous Allocations Still a Problem?

Count humongous allocation events in your GC log (as root):

grep "Humongous Allocation" /opt/zimbra/log/gc.log | wc -l

After doubling the region size, this count should drop significantly. A handful of humongous events per day is acceptable. Dozens per hour indicates objects are still exceeding the threshold and you may want to consider 16m regions, or investigate what Zimbra component is producing the large allocations.


Step 6: Tune the Initiating Heap Occupancy Percent

The Parameter

-XX:InitiatingHeapOccupancyPercent=35

This controls when G1 starts a concurrent marking cycle — the background process that identifies which objects in old gen are reclaimable. The default is 45, meaning concurrent marking doesn’t begin until the heap is 45% occupied.

In practice, on a large heap with sustained allocation (multiple users, multiple clients, large attachments), 45% can be too late. By the time marking finishes and mixed GC cycles can actually reclaim old gen memory, the heap has already climbed much higher — sometimes to 90%+ before GC fires, leaving almost no room for To-space.

Lowering IHOP to 35 starts the concurrent marking cycle earlier, giving G1 more time to complete marking and begin reclaiming old gen before the heap fills. The trade-off is slightly more CPU used for background marking, which on a mail server is almost always the right trade-off.

Rule of Thumb

  • Default (45%): Fine for applications with moderate, predictable allocation rates
  • 35%: Recommended for Zimbra environments with large mailboxes and multiple client types
  • 25–30%: Consider if you’re seeing old gen grow continuously even with IHOP=35, or if your server has a high ratio of IMAP/ActiveSync clients to CPU cores

Do not set IHOP below 25% without careful monitoring — too-frequent concurrent marking adds CPU overhead without benefit if old gen isn’t actually growing fast enough to need it.

Test: Is IHOP Working?

With the correct IHOP setting, you should see regular Concurrent Mark Cycle entries in zmmailboxd.out, and old gen (the post-GC heap size on Pause Young lines) should stabilise rather than climbing continuously:

GC(n) Concurrent Mark Cycle
GC(n) Pause Remark 2100M->1950M(7896M) 45ms
GC(n) Pause Cleanup 1950M->1950M(7896M) 0.2ms
GC(n) Concurrent Mark Cycle 2800ms

If you’re seeing Pause Young post-GC values climbing by 50–100 MB every few GC cycles with no concurrent mark cycles firing, IHOP is too high for your workload.

Test: Monitoring Old Gen Growth

A quick way to watch old gen trend in real time (as root):

grep "Pause Young (Normal)" /opt/zimbra/log/gc.log | \
  awk '{print $NF, $(NF-1)}' | tail -50

The right-hand number after the arrow in each line (e.g. 1600M in 4800M->1600M(7896M)) is the heap size immediately after GC. This value is dominated by live old gen objects. If it’s trending upward over hours without ever coming back down, old gen is accumulating faster than it’s being reclaimed. Lowering IHOP or enabling more concurrent GC threads (see below) is the fix.


Step 7 (Optional): Increase Concurrent GC Threads

The Parameter

-XX:ConcGCThreads=2

G1’s concurrent marking runs in background threads while the application continues. The default number of concurrent GC threads is calculated as roughly max(1, ParallelGCThreads / 4). On a 4-CPU server, this often means just 1 concurrent marking thread — which may not be fast enough to complete marking before the heap fills under heavy load.

Check your current value at startup (as root):

grep -i "Concurrent Workers" /opt/zimbra/log/gc.log | head -5

If you see Concurrent Workers: 1 on a server with 4 or more CPUs, you may benefit from explicitly setting this to 2.

Rule of Thumb

Available CPUs Recommended ConcGCThreads
2 1 (default is fine)
4 2
8 2–3
16+ 3–4

Don’t set this too high — concurrent GC threads compete with application threads for CPU. On a busy mail server, using more than 25–30% of your CPU cores for concurrent marking will hurt throughput.


Putting It All Together

Here is a complete example mailboxd_java_options string incorporating all of the above recommendations, for a server with an ~8 GB heap. Be sure to back up your existing mailboxd_java_options value before running the command below!

zmlocalconfig -e mailboxd_java_options="-server \
  -Dhttps.protocols=TLSv1.2,TLSv1.3 \
  -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3 \
  -Djava.awt.headless=true \
  -Dsun.net.inetaddr.ttl=${networkaddress_cache_ttl} \
  -Dorg.apache.jasper.compiler.disablejsr199=true \
  -XX:+UseG1GC \
  -XX:SoftRefLRUPolicyMSPerMB=1 \
  -XX:+UnlockExperimentalVMOptions \
  -XX:G1ReservePercent=20 \
  -XX:G1NewSizePercent=10 \
  -XX:G1MaxNewSizePercent=30 \
  -XX:G1HeapRegionSize=8m \
  -XX:InitiatingHeapOccupancyPercent=35 \
  -XX:-OmitStackTraceInFastThrow \
  -verbose:gc \
  -Xlog:gc*=info,safepoint=info:file=/opt/zimbra/log/gc.log:time:filecount=20,filesize=10m \
  -Djava.security.egd=file:/dev/./urandom \
  --add-opens java.base/java.lang=ALL-UNNAMED \
  -Djava.net.preferIPv4Stack=true \
  -Dcom.redhat.fips=false"

Note: The base options (-Dhttps.protocols, –add-opens, -Dcom.redhat.fips, etc.) should match what your Zimbra version ships with — don’t blindly copy the above if your version differs. Use zmlocalconfig mailboxd_java_options to get your current string and surgically add/edit only the tuning parameters discussed in this blog post!

Note Also: Zimbra includes -verbose:gc by default. On modern JDKs, unified logging (-Xlog:gc…) supersedes legacy GC logging flags; keeping -verbose:gc is harmless but redundant.

Then restart mailboxd:

zmmailboxdctl restart

After the Restart: What to Monitor

Give the server at least 24–48 hours of normal production load before drawing conclusions.

# Check for any To-space exhaustion events (should be zero):
grep "To-space exhausted" /opt/zimbra/log/gc.log

# Count humongous allocation triggers (should be low):
grep "Humongous Allocation" /opt/zimbra/log/gc.log | wc -l

# Watch pause time trend (should mostly be under 100ms):
grep "Pause Young (Normal)" /opt/zimbra/log/gc.log | grep -oP '\d+\.\d+ms$' | sort -n | tail -20

# Confirm concurrent marking is firing regularly:
grep "Concurrent Mark Cycle$" /opt/zimbra/log/gc.log | wc -l

Signs that tuning is working well:

  • No To-space exhausted events
  • Pause times consistently under 100 ms
  • Old gen (post-GC heap values) stabilising, not climbing indefinitely
  • Regular concurrent mark cycles completing in the background
  • Humongous allocation events rare or absent

Signs that further tuning is needed:

  • Old gen still climbing continuously → lower InitiatingHeapOccupancyPercent further, or add ConcGCThreads=2
  • Humongous allocations still frequent → increase G1HeapRegionSize to 16m
  • Pause times spiking occasionally into 200–500 ms → lower G1MaxNewSizePercent and/or raise G1ReservePercent to 25

A Note on Heap Size Reduction

Once GC tuning is stable and you can see where old gen actually settles over several days of production load, you may find you can reduce mailboxd_java_heap_size. A rough formula:

Minimum safe heap = (stable old gen size) × 1.5 + (max young gen size)

For example, if old gen stabilises at 2.5 GB and your max young gen is ~2.4 GB (30% of 8 GB), a 7–8 GB heap has comfortable headroom. If old gen stabilises at 1.5 GB, you could safely run a 5–6 GB heap and reallocate the freed RAM to MariaDB’s buffer pool, which benefits Zimbra’s folder and message metadata lookups.

Do not reduce heap size speculatively — always baseline first with several days of GC log data.


Quick Reference

Parameter Default Recommended Why
mailboxd_java_heap_size 30% of RAM 25–30% of RAM, max ~8 GB Prevents OS swapping
G1ReservePercent 10 20 Reserves emergency To-space, prevents exhaustion
G1NewSizePercent 5 10 Prevents young gen from being too small
G1MaxNewSizePercent 60 30 Caps young gen, preserves To-space
G1HeapRegionSize 4m (on 8GB heap) 8m Raises humongous threshold to 4MB
InitiatingHeapOccupancyPercent 45 35 Starts concurrent marking earlier
ConcGCThreads 1 (on 4-CPU) 2 (on 4-CPU) Speeds up background marking

Bonus Java Tuning Suggestion!

All of the above tuning suggestions are for mailboxd. But there’s another Java tuning knob worth turning: the localconfig attribute zimbra_zmjava_options.

This option controls Java when you run a command like zmprov sa zimbraMailHost=mb31.example.com. This option also impacts zmbackup, zmrestore, and zmblobchk.

Most times, you’ll get an “out of heap” error when you run a command like that, because there’s a separate Java heap for those commands. Increasing the localconfig attribute mailboxd_java_heap_size will do nothing to get rid of such errors!

The trick is to increase the Xmx value in the localconfig attribute zimbra_zmjava_options. The default is set to 256m — way too small in most cases. Fix it like so:

zmlocalconfig -e zimbra_zmjava_options="-Xmx1024m -Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3 -Djava.net.preferIPv4Stack=true"

Need Help?

If you’d like help tuning your Zimbra server(s), just start the conversation by filling out this form:

← Back

Thank you for your response. ✨

 

Hope that helps,
L. Mark Stone
Mission Critical Email LLC
26 February 2026

A sincere “Thank you!” to Matthew Francis at In-Tuition Networks for your considered review and terrific suggestions for improving this blog post.

The information provided in this blog is intended for informational and educational purposes only. The views expressed herein are those of Mr. Stone personally. The contents of this site are not intended as advice for any purpose and are subject to change without notice. Mission Critical Email makes no warranties of any kind regarding the accuracy or completeness of any information on this site, and we make no representations regarding whether such information is up-to-date or applicable to any particular situation. All copyrights are reserved by Mr. Stone. Any portion of the material on this site may be used for personal or educational purposes provided appropriate attribution is given to Mr. Stone and this blog.

Leave a Reply