logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·02.11.26
← Back to news

Tune Code Before Your Garbage Collector

Original article
blog.vanillajava.blog|12 points|3 comments|by peter_lawrey|Jul 8, 2026

Prioritize Code Tuning Over Garbage Collector Tweaking

Understanding the inner workings of Core Java is the secret to developing applications that are not only simpler but significantly faster.

The Core Thesis

Optimizing how your application handles memory allocations can yield far greater performance gains than simply swapping your Garbage Collector (GC). More importantly, picking a GC first optimizing your code can actually change which GC is the most efficient for your specific workload.

The Benchmark Scenario

To demonstrate this, a latency benchmark was conducted measuring the time from a MarketDataSnapshot to a NewOrderSingle.

  • Throughput: 50,00050,00050,000 messages per second.
  • Duration: 30 minutes.
  • Tooling: JLBH testing Chronicle-FIX.

The experiment compared a system performing redundant work (logging every message via SLF4J) against a streamlined system (relying on Chronicle Queue for internal recording).


Phase 1: The Unoptimized Benchmark

In this version, a single SLF4J log line was added to every message sent. While one line seems negligible, at high frequencies in a low-latency environment, the impact is massive.

RTT Latency Distribution (with SLF4J logging)

GC OptionP99P_{99}P99​ (μs\mu sμs)P99.99P_{99.99}P99.99​ (μs\mu sμs)
Parallel (Large Eden, no large pages)16.8620,480
ZGC (no large pages)12.2119,694
G1 + COH (no large pages)12.0220,349
G1 (2 MiB large pages)13.9420,021
Shenandoah Generational (no large pages)12.3019,235

Note: The P99.99P_{99.99}P99.99​ values are in thousands of microseconds, meaning the worst 1 in 10,000 events take roughly 19–20ms19\text{--}20\text{ms}19–20ms. At 50K/s50\text{K/s}50K/s, this happens ≈5\approx 5≈5 times per second.

At this stage, one might conclude that Shenandoah is the superior choice and Parallel GC should be avoided.


Phase 2: Assessing Disk I/O Impact

Since I/O is a common source of jitter, the logs were moved to /dev/shm (a RAM-backed file system) to see if the storage medium was the culprit.

RTT Latency Distribution (logging to /dev/shm)

GC OptionP99P_{99}P99​ (μs\mu sμs)P99.99P_{99.99}P99.99​ (μs\mu sμs)
Parallel (Large Eden, no large pages)34.5010,994
ZGC (no large pages)10.869,617
G1 + COH (no large pages)10.9010,043
G1 (2 MiB large pages)10.6411,682
Shenandoah Generational (no large pages)10.329,552

While the P99P_{99}P99​ for Parallel GC looks like an outlier here, the trend remains: Shenandoah still looks attractive, but the act of changing the I/O path provided a more significant boost than changing the GC.


Phase 3: Removing Redundant Logging

Because Chronicle-FIX already records messages internally, the SLF4J logs were entirely redundant. In high-performance coding, we avoid standard logging in favor of Chronicle-Queue.

Our Logging Policy:

  • No INFO level logging after the application has started.
  • ERROR/WARN levels are enabled by default.
  • DEBUG logging is disabled by default.
  • Every log line must be strictly justified.

RTT Latency Distribution (Chronicle-Queue recording only)

GC OptionP99P_{99}P99​ (μs\mu sμs)P99.99P_{99.99}P99.99​ (μs\mu sμs)
Parallel (Large Eden, no large pages)7.148.94
ZGC (no large pages)7.829.36
G1 + COH (no large pages)8.3012.4
G1 (2 MiB large pages)8.59154.4
Shenandoah Generational (no large pages)7.83233.7

The Result

The P99P_{99}P99​ dropped by about 2μs2\mu s2μs, but the P99.99P_{99.99}P99.99​ plummeted by three orders of magnitude. Most strikingly, the "best" GC flipped: Parallel GC is now the winner, while Shenandoah performed the worst at the P99.99P_{99.99}P99.99​ level.


Final Conclusion

The ideal Garbage Collector is not a universal constant; it is dependent on your specific workload.

  1. Optimize the workload first.
  2. Then choose the GC.

Doing this ensures you don't waste time tuning a GC to compensate for inefficient code, and it ensures you pick the GC that actually fits your optimized profile.


🖼️ Visual Evidence


📚 Other Popular Posts

🤖 Comparing AI for Code Optimization

Different AI models provide varied results when optimizing the same snippet. The best strategy is to query multiple AIs and select the most elegant solution.

Example Challenge: Optimizing a formatOffset method.

private static String formatOffset(int millis) {
    String sign = millis < 0 ? "-" : "+";
    int saveSecs = Math.abs(millis) / 1000;
    int hours = saveSecs / 3600;
    int mins = ((saveSecs / 60) % 60);
    int secs = (saveSecs % 60);
    if (secs == 0) {
        if (mins == 0) {
            return sign + twoDigitString(hours);
        }
        return sign + twoDigitString(hours) + twoDigitString(mins);
    }
    return sign + twoDigitString(hours) + twoDigitString(mins) + twoDigitString(secs);
}

🔍 Java Object Sizes Demystified

Measuring memory in Java is tricky because the JVM abstracts raw memory. However, for low-latency systems, understanding Compressed Oops (Ordinary Object Pointers) and Compact Object Headers is vital for reasoning about the actual memory footprint of your objects.