Using the Agrona library (Part 1)
Why Agrona? The JDK's Performance Gaps
"Java's 'HashMap<Integer, Integer>' allocates 4 objects per entry. Agrona's
Int2IntHashMapallocates zero."
Before diving into Agrona's components, you need to understand why this library exists. The JDK's standard library is excellent for general-purpose development, but it has fundamental design constraints that make it unsuitable for low-latency, high-throughput systems. Agrona fills those gaps.
What is Agrona?
Agrona is a high-performance Java utility library created by Real Logic — the company behind Aeron (ultra-low-latency messaging) and SBE (Simple Binary Encoding). It provides data structures and utilities that the JDK doesn't offer, designed for:
Zero-allocation operations in steady state
Off-heap memory access with proper memory ordering
Lock-free concurrent data structures
Primitive-typed collections (no boxing)
Mechanical sympathy — working with the hardware, not against it
Problem 1: Boxing — The Hidden Allocation Tax
Java generics don't work with primitives. HashMap<int, int> is illegal — you must use HashMap<Integer, Integer>.
Every int → Integer conversion (autoboxing) creates an object on the heap:
// ❌ JDK: Every put() creates at least 2 Integer objects + 1 Node
HashMap<Integer, Integer> jdkMap = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) {
jdkMap.put(i, i * 2); // Integer.valueOf(i) + Integer.valueOf(i*2) + new Node
}
Memory Comparison for 1 Million int→int Mappings
JDK HashMap<Integer,Integer> |
Agrona Int2IntHashMap |
|
|---|---|---|
| Objects per entry | 3 (Node + 2 Integers) | 0 |
| Bytes per entry | ~64 | ~8 |
| Total memory | ~64 MB | ~8 MB |
| GC overhead | 3 million objects to track | 0 objects |
| Ratio | 1x | 8x less memory |
// ✅ Agrona: Zero allocation, zero boxing
import org.agrona.collections.Int2IntHashMap;
Int2IntHashMap agronaMap = new Int2IntHashMap(Integer.MIN_VALUE); // missingValue
for (int i = 0; i < 1_000_000; i++) {
agronaMap.put(i, i * 2); // No boxing! No object creation!
}
int value = agronaMap.get(42); // Returns primitive int, no unboxing
Problem 2: GC Pressure — Death by a Thousand Allocations
Every object you allocate is eventually garbage collected. In a trading system processing millions of messages per second, even tiny allocations create massive GC pressure:
Agrona's Zero-Allocation Promise
Agrona data structures are designed so that after initialization, the steady-state processing path allocates zero objects:
// ✅ After construction, NO allocations in the hot path
Int2IntHashMap map = new Int2IntHashMap(Integer.MIN_VALUE);
map.put(1, 100); // No allocation — stores ints directly in internal array
map.get(1); // No allocation — returns primitive int
// ✅ Iteration without Iterator objects (using lambda)
map.forEach((key, value) -> {
// No Iterator object created!
process(key, value);
});
Compare with JDK collections, which allocate Iterator objects on every for-each, Entry objects on every entrySet() call, and Integer wrappers on every access.
Problem 3: No Off-Heap Data Structures
The JDK provides ByteBuffer for raw memory access, but has no higher-level off-heap data structures. Agrona provides:
Off-heap structures have critical advantages:
No GC pauses — GC doesn't scan native memory
Memory-mapped file backing — survives process restarts
IPC capable — multiple processes can share the same memory
Predictable latency — no stop-the-world pauses
Problem 4: No Lock-Free IPC Primitives
The JDK's concurrent collections (ConcurrentHashMap, BlockingQueue) are designed for inter-thread communication. Agrona provides primitives for inter-process communication (IPC):
Agrona's ring buffers and broadcast buffers work across process boundaries via memory-mapped files, with lock-free CAS-based coordination.
Problem 5: No Agent Threading Model
The JDK gives you Thread, ExecutorService, and ForkJoinPool — all designed for work-stealing and blocking IO. None of them support the busy-spin or duty-cycle patterns needed for ultra-low-latency processing:
Agrona's Agent + AgentRunner + IdleStrategy provide a composable framework for building event-loop style services with tunable CPU/latency trade-offs.
The Agrona Design Philosophy
Core Principles
No allocation in the hot path — Preallocate everything. Reuse buffers. Never create objects during message processing.
Primitives over objects — Avoid autoboxing. Store
intandlongvalues directly in arrays, not wrapped inIntegerandLongobjects.Flat memory layouts — Use arrays, not linked structures. Arrays are cache-friendly; linked nodes scatter data across the heap.
Off-heap when needed — Use native memory for IPC, telemetry, and any structure that must outlive GC cycles.
Measure, don't guess — Agrona includes benchmarks for every data structure. Performance claims are backed by JMH measurements.
Agrona's Component Map
Getting Started
Maven
<dependency>
<groupId>org.agrona</groupId>
<artifactId>agrona</artifactId>
<version>2.4.0</version>
</dependency>
Gradle
implementation 'org.agrona:agrona:2.4.0'
Quick Taste
import org.agrona.collections.Int2IntHashMap;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.ringbuffer.OneToOneRingBuffer;
// 1. Primitive map — no boxing!
Int2IntHashMap priceMap = new Int2IntHashMap(Integer.MIN_VALUE);
priceMap.put(1001, 15025); // instrumentId → price in cents
int price = priceMap.get(1001); // returns 15025, no Integer object
// 2. Off-heap buffer — direct memory access
UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocateDirect(1024));
buffer.putLong(0, System.nanoTime()); // Write long at offset 0
buffer.putInt(8, 42); // Write int at offset 8
buffer.putStringAscii(12, "Hello Agrona"); // Write string at offset 12
long timestamp = buffer.getLong(0); // Read back — zero allocation
// 3. Ring buffer — lock-free IPC
ByteBuffer backingBuffer = ByteBuffer.allocateDirect(
OneToOneRingBuffer.TRAILER_LENGTH + (1024 * 16)
);
OneToOneRingBuffer ringBuffer = new OneToOneRingBuffer(
new UnsafeBuffer(backingBuffer)
);
// Producer
UnsafeBuffer msg = new UnsafeBuffer(new byte[64]);
msg.putStringAscii(0, "TRADE:AAPL:100@150.25");
ringBuffer.write(1, msg, 0, msg.capacity());
// Consumer
ringBuffer.read((msgTypeId, buffer1, index, length) -> {
String trade = buffer1.getStringAscii(index);
System.out.println("Received: " + trade);
});
Github Repo : https://github.com/sachin-handiekar/blog/tree/master/agrona-code
Summary
When to use Agrona:
Processing millions of events per second
GC pauses are unacceptable (trading, gaming, real-time)
You need inter-process communication on a single machine
You're building infrastructure (messaging, telemetry, caching)
Memory efficiency matters (primitive-heavy workloads)
When to stick with JDK:
General application development
GC pauses are acceptable
Team familiarity matters more than raw performance
You don't have latency SLAs in the microsecond range