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

Why low-latency Java still requires discipline?

Original article
chronicle.software|61 points|33 comments|by theanonymousone|Jul 6, 2026

Why Low-Latency Java Still Requires Discipline

Java is often dismissed in the high-frequency trading (HFT) and ultra-low-latency world because of its managed nature. However, modern JVMs have evolved. The common misconception is that with the advent of "pause-less" garbage collectors, the developer can return to a state of relaxed coding. This is a dangerous fallacy. To achieve deterministic performance, discipline is not optional; it is the primary requirement.

The Illusion of the "Pause-less" JVM

Many developers believe that switching to ZGC or Shenandoah eliminates the need for memory management. While these collectors drastically reduce Stop-the-World (STW) pauses, they do not eliminate the cost of memory pressure.

"A garbage collector is not a magic wand; it is a debt collection agency. Eventually, the bill for your allocations must be paid."

If the allocation rate exceeds the reclamation rate, the JVM hits an allocation stall, which is often more catastrophic than a scheduled GC pause.

Comparison of GC Strategies

CollectorPrimary GoalTrade-offLatency Profile
G1GCBalanced throughputHigher pause timesMillisecond range\text{Millisecond range}Millisecond range
ZGCUltra-low latencyLower throughputSub-millisecond\text{Sub-millisecond}Sub-millisecond
EpsilonNo GC (No-op)Memory exhaustionZero GC overhead\text{Zero GC overhead}Zero GC overhead

Mechanical Sympathy: Understanding the Hardware

Low-latency Java requires Mechanical Sympathy—the practice of writing software that aligns with the underlying hardware architecture. The JVM abstracts the hardware, but the CPU does not.

The Cache Hierarchy and False Sharing

Modern CPUs fetch data in cache lines (typically 64 bytes). If two threads modify different variables that happen to reside on the same cache line, the CPU triggers "cache coherence traffic," forcing the cores to synchronize. This is known as False Sharing.

To prevent this, disciplined developers use the @Contended annotation or manual padding to ensure hot variables reside on separate cache lines.

The JIT Warm-up and Determinism

Java's performance is a journey, not a starting point. The Just-In-Time (JIT) compiler optimizes code based on profiling data. This creates a "warm-up" period where the first few thousand executions are significantly slower than subsequent ones.

The Deoptimization Trap

If the JVM makes an optimistic assumption about a code path (e.g., "this interface only ever has one implementation") and that assumption is later proven wrong, the JVM performs a deoptimization. It throws away the compiled machine code and reverts to interpreted mode.

// Disciplined approach: Pre-warming the JVM
public void warmUp() {
    for (int i = 0; i < 10_000; i++) {
        // Execute critical paths with dummy data 
        // to trigger C2 compilation before live traffic hits.
        processOrder(dummyOrder); 
    }
}

Data Layout and the Cost of Boxing

One of the greatest enemies of latency is the Object header. Every object in Java carries metadata, adding overhead and destroying spatial locality.

The Boxing Tax

Using wrapper classes like Integer or Double instead of primitives leads to pointer chasing.

  • List<Integer> →\rightarrow→ Bad: Array of pointers to Integer objects scattered in heap.
  • int[] →\rightarrow→ Good: Contiguous block of memory.

The mathematical cost of a cache miss can be represented as: Total Latency=Execution Time+(Cache Misses×Miss Penalty)\text{Total Latency} = \text{Execution Time} + (\text{Cache Misses} \times \text{Miss Penalty})Total Latency=Execution Time+(Cache Misses×Miss Penalty) Where the Miss Penalty\text{Miss Penalty}Miss Penalty for main memory can be ≈100ns\approx 100\text{ns}≈100ns, compared to ≈1ns\approx 1\text{ns}≈1ns for L1.

Memory Layout Comparison

The Low-Latency Discipline Checklist

To maintain a deterministic system, developers must adhere to a strict set of constraints.

  • Zero Allocation in the Hot Path: No new keywords inside the main loop.
  • Primitive Collections: Use libraries like Agrona or fastutil instead of java.util.
  • Lock-Free Concurrency: Prefer AtomicLong or the Disruptor Pattern over synchronized blocks.
  • Off-Heap Memory: Use ByteBuffer.allocateDirect() or sun.misc.Unsafe to bypass GC entirely.
  • CPU Affinities: Pin threads to specific cores to avoid OS context switching.

Conclusion

Java can be incredibly fast, but it requires the developer to fight the language's natural tendencies. You must treat the JVM not as a managed environment, but as a tool to generate efficient machine code. By applying mechanical sympathy, avoiding allocation, and managing the JIT lifecycle, you can achieve C-like performance with Java's productivity.

The discipline is the difference between a system that is usually fast and a system that is consistently deterministic.