Skip to main content

Command Palette

Search for a command to run...

Using the Agrona library (Part 1)

Why Agrona? The JDK's Performance Gaps

Published
6 min read

"Java's 'HashMap<Integer, Integer>' allocates 4 objects per entry. Agrona's Int2IntHashMap allocates 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 intInteger 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

  1. No allocation in the hot path — Preallocate everything. Reuse buffers. Never create objects during message processing.

  2. Primitives over objects — Avoid autoboxing. Store int and long values directly in arrays, not wrapped in Integer and Long objects.

  3. Flat memory layouts — Use arrays, not linked structures. Arrays are cache-friendly; linked nodes scatter data across the heap.

  4. Off-heap when needed — Use native memory for IPC, telemetry, and any structure that must outlive GC cycles.

  5. 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