Low-Latency Java Programming: Best Practices Unleashed
Achieving ultra-low latency in Java (think microseconds or sub-millisecond tail latencies) is challenging but very feasible with the right techniques. This is critical for high-frequency trading, real
1. Avoid Garbage Collection (GC) as much as possible
Use object pools / preallocation: Avoid allocating objects on the fly. Reuse them where possible.
Use off-heap memory: Tools like Chronicle Queue, Agrona, or DirectByteBuffer help store data off-heap.
Choose the right GC:
ZGC and Shenandoah: Low pause GC (not ultra-low, but good for soft real-time).
For ultra-low: Use Epsilon GC for benchmark/test (no GC), or manually manage memory.
Consider NoGC design: allocate everything at startup, and never let GC kick in.
2. Tune the JVM
Use these flags (tune per app):
-XX:+UseNUMA // https://answers.ycrash.io/question/-what-is-jvm-startup-parameter--xxusenuma?q=851
-XX:+UseLargePages
-XX:+AlwaysPreTouch
-XX:MaxInlineSize=1024
-XX:+UseCompressedOops (disable if using more than 32 GB heap)
//There are a lot of JVM JIT compiler optimization techniques, the most important of which is inlining. Method inlining saves the creation of method stack frames, and method inlining also enables more and deeper optimizations by the JIT compiler.Pin threads to cores using
tasksetornumactl(on Linux).Avoid context switches — ensure thread affinity with libraries like
JaffreorAffinity.
3. Use Efficient Data Structures
Prefer primitive collections from libraries like Agrona, fastutil, or HPPC.
Avoid autoboxing/unboxing.
Prefer flat data structures (no nested objects).
4. Threading & Locking
Avoid locks:
Use lock-free algorithms:
java.util.concurrent’sAtomic*classes orsun.misc.Unsafe.Use Disruptor pattern (from LMAX Disruptor library) — extremely low-latency pub-sub model.
Use busy spinning (
Thread.onSpinWait()in Java 9+) instead of blocking in ultra-low paths.
5. Time & Latency Measurement
Use nanosecond timers:
System.nanoTime()(notcurrentTimeMillis()).Warm up the JVM before measuring (due to JIT compilation).
Profile using JMH, perf, Flame Graphs, and Java Flight Recorder.
6. Networking Optimizations
Use kernel bypass tech for I/O: DPDK, Solarflare, or RDMA.
Avoid NIO unless fine-tuned; consider netty, Chronicle Wire, or Aeron for faster messaging.
Use binary protocols (not JSON/XML), e.g., SBE (Simple Binary Encoding).
7. Libraries & Frameworks for Low Latency
LMAX Disruptor – Lock-free inter-thread messaging.
Chronicle Queue/Wire – Off-heap, zero-GC persisted messaging.
Aeron – Low-latency UDP messaging.
Agrona – Utilities for concurrency, collections, buffers.
8. Keep it Simple
Code predictability beats cleverness — branch prediction matters.
Avoid exceptions for flow control.
Minimize false sharing by using
@Contended(Java 8+).
Example: Fast Pub/Sub using Disruptor
RingBuffer<MyEvent> ringBuffer = RingBuffer.createSingleProducer(MyEvent::new, 1024);
EventHandler<MyEvent> handler = (event, sequence, endOfBatch) -> {
// process event
};
Disruptor<MyEvent> disruptor = new Disruptor<>(MyEvent::new, 1024, Executors.defaultThreadFactory(), ProducerType.SINGLE, new BusySpinWaitStrategy());
disruptor.handleEventsWith(handler);
disruptor.start();Sample Ultra-Low Latency Trading Engine Loop in Java
Here’s a simplified event-driven trading engine loop using LMAX Disruptor — great for inter-thread messaging with ultra-low latency:
import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.concurrent.Executors;
// Market data or order event
class MarketEvent {
double price;
int quantity;
long timestamp;
}
class MarketEventFactory implements EventFactory<MarketEvent> {
public MarketEvent newInstance() {
return new MarketEvent();
}
}
// Handler (core of trading logic)
class TradingEventHandler implements EventHandler<MarketEvent> {
@Override
public void onEvent(MarketEvent event, long sequence, boolean endOfBatch) {
// Minimalistic ultra-fast processing
if (event.price < 100.5) {
// Example logic: send buy order
System.out.printf("Buy signal at %.2f qty %d%n", event.price, event.quantity);
}
}
}
public class UltraLowLatencyEngine {
public static void main(String[] args) throws Exception {
int ringBufferSize = 1024;
Disruptor<MarketEvent> disruptor = new Disruptor<>(
new MarketEventFactory(),
ringBufferSize,
Executors.defaultThreadFactory(),
ProducerType.SINGLE,
new BusySpinWaitStrategy() // low-latency spin wait
);
disruptor.handleEventsWith(new TradingEventHandler());
disruptor.start();
RingBuffer<MarketEvent> ringBuffer = disruptor.getRingBuffer();
// Simulated incoming market feed
while (true) {
long seq = ringBuffer.next();
try {
MarketEvent evt = ringBuffer.get(seq);
evt.price = 100 + Math.random();
evt.quantity = (int)(Math.random() * 10);
evt.timestamp = System.nanoTime();
} finally {
ringBuffer.publish(seq);
}
// Remove sleep in real trading, added here to simulate pacing
Thread.sleep(1); // For demo only
}
}
}Latency Profiling & Analysis Tools for Java on Linux
Here's a quick list of tools used by low-latency trading firms and performance engineers:
Java-Specific Tools
Native/Linux Profiling Tools
Example: Ultra-Low Latency Java Engine with Order Book + FIX Parser
Project Setup (Maven)
<dependencies>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
</dependencies>
1. Market Order Event
public class OrderEvent {
String orderId;
char side; // 'B' = Buy, 'S' = Sell
double price;
int quantity;
long timestamp;
}2. Lightweight FIX Parser (Simulated)
Real FIX would use tag-value format (e.g., 35=D|55=MSFT|54=1|38=100|44=101.5). We'll simplify:
public class FixParser {
public static OrderEvent parse(String fixMessage) {
// Example: "ID=O123|SIDE=B|QTY=100|PRICE=101.5"
OrderEvent event = new OrderEvent();
String[] tokens = fixMessage.split("\\|");
for (String token : tokens) {
String[] pair = token.split("=");
switch (pair[0]) {
case "ID": event.orderId = pair[1]; break;
case "SIDE": event.side = pair[1].charAt(0); break;
case "QTY": event.quantity = Integer.parseInt(pair[1]); break;
case "PRICE": event.price = Double.parseDouble(pair[1]); break;
}
}
event.timestamp = System.nanoTime();
return event;
}
}3. In-Memory Order Book
import java.util.*;
public class OrderBook {
private final TreeMap<Double, Integer> bids = new TreeMap<>(Collections.reverseOrder());
private final TreeMap<Double, Integer> asks = new TreeMap<>();
public void addOrder(OrderEvent order) {
TreeMap<Double, Integer> book = order.side == 'B' ? bids : asks;
book.merge(order.price, order.quantity, Integer::sum);
}
public void printTopLevels() {
System.out.println("Top of Book:");
System.out.println("Best Bid: " + (bids.isEmpty() ? "-" : bids.firstEntry()));
System.out.println("Best Ask: " + (asks.isEmpty() ? "-" : asks.firstEntry()));
}
}
4. Disruptor Event Handling
import com.lmax.disruptor.*;
class OrderEventFactory implements EventFactory<OrderEvent> {
public OrderEvent newInstance() {
return new OrderEvent();
}
}
class OrderHandler implements EventHandler<OrderEvent> {
private final OrderBook book;
public OrderHandler(OrderBook book) {
this.book = book;
}
public void onEvent(OrderEvent event, long seq, boolean endOfBatch) {
book.addOrder(event);
if (seq % 10 == 0) { // Print every 10 orders
book.printTopLevels();
}
}
}
5. Engine Startup
import com.lmax.disruptor.dsl.Disruptor;
import java.util.concurrent.Executors;
public class TradingEngine {
public static void main(String[] args) {
int bufferSize = 1024;
OrderBook orderBook = new OrderBook();
Disruptor<OrderEvent> disruptor = new Disruptor<>(
new OrderEventFactory(),
bufferSize,
Executors.defaultThreadFactory(),
ProducerType.SINGLE,
new BusySpinWaitStrategy()
);
disruptor.handleEventsWith(new OrderHandler(orderBook));
disruptor.start();
RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
// Simulate incoming FIX messages
String[] fixMsgs = {
"ID=O1|SIDE=B|QTY=100|PRICE=101.5",
"ID=O2|SIDE=S|QTY=150|PRICE=101.8",
"ID=O3|SIDE=B|QTY=200|PRICE=101.6",
"ID=O4|SIDE=S|QTY=180|PRICE=101.4"
};
for (String msg : fixMsgs) {
OrderEvent parsed = FixParser.parse(msg);
long seq = ringBuffer.next();
try {
OrderEvent event = ringBuffer.get(seq);
event.orderId = parsed.orderId;
event.side = parsed.side;
event.price = parsed.price;
event.quantity = parsed.quantity;
event.timestamp = parsed.timestamp;
} finally {
ringBuffer.publish(seq);
}
}
// keep alive to observe output
try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
}
}
Output Example
Top of Book:
Best Bid: 101.6=200
Best Ask: 101.4=180Optional Enhancements
Matching Engine Logic — match orders before adding to book.
FIX Engine — integrate with QuickFIX/J for full FIX support.
Persistence — integrate Chronicle Queue to log events.
JMH Benchmarks — profile
addOrder()orparse()methods.




