NoHeapDB
Off-heap storage for Java 26

Gigabytes in your JVM. Nothing for the GC to scan.

NoHeapDB is an embedded key-value, document and graph store that keeps its data inside your process but outside the Java heap, in MemorySegments from the Foreign Function & Memory API. Load 10 GB, run with an 8 MB heap, and read records in about a microsecond.

git clone https://github.com/ericjbruno/NoHeapDB && mvn install -DskipTests
One JVM process -Xmx8m
Java heap3.4 MB of 8 MB
Off-heap journal163,840 records · 10 GB
~24,700verified 64 KB reads per second
1.0 µsaverage zero-copy read
0full or old-generation GCs
Figures from NoHeapDBLargeDataTest: 10 GB of 64 KB records, 90 s of random reads, G1, every byte checked.
Gigabytesof records behind an very small heap (peak heap under 4 MB)
12.1 Mreads per second, 20 million records, -Xmx32m, one thread
412young GCs in 90 s of reads, down from 62,332 before zero-copy views
0wrong values across 20 million random UUID keys

Measured on the project's own test programs. Your numbers depend on record size, key shape and hardware.

Why off-heap

Big in-process data and the garbage collector don't mix well.

Put millions of objects in a heap cache and the collector has to trace them. Pauses and GC CPU grow with the heap. The usual escape is an external store such as Redis, which moves the cost to a network hop and a serialization step on every read.

NoHeapDB takes a third path. The data stays in your address space, so reads are direct memory access. But it lives in native memory the collector never traces, so a 10 GB store costs the GC the same as an empty one.

It's pure Java with no JNI, built on the FFM API that became final in Java 22: MemorySegment for the memory and Arena to free it the moment a store is deleted.

On-heap cache
HashMap, Caffeine
External store
Redis, Memcached
NoHeapDB
Where data livesJava heapAnother processYour process, off-heap
GC work grows with dataYesNoNo
Cost of a readPointer chaseRound trip and deserializationDirect memory read; zero-copy views
Survives a restartNoIf configuredOptional, as mapped files
Extra to runNothingA serverNothing
Features

Everything in the box

Three data models over one off-heap engine, with the storage details handled for you.

Off-heap storage

  • Native memory, not heapRecords live in MemorySegment chunks the collector never traces.
  • No 2 GB ceilingChunks of up to 4 GB, up to 65,536 per store. Growth adds a chunk and copies nothing.
  • Freed when you say soEach store has its own Arena; deleting the store releases its memory at once.
  • No JVM flagsArena memory isn't capped by -XX:MaxDirectMemorySize.

Persistence & recovery

  • In memory or on diskIN_MEMORY stores, or PERSISTED stores backed by memory-mapped files.
  • Close and reopenClosing flushes and unmaps; creating the store again reopens its files, in a new JVM too.
  • Index rebuilt after a crashEvery record holds its key, so an unclean shutdown rebuilds the index from the journal.
  • Newest version winsVersion stamps resolve an overwrite caught mid-way by a crash.

Key-value API

  • Typed puts and getsString, Integer, Long, Short, Float, Double, char and any Serializable object.
  • Exact key matchingKeys up to 32,767 bytes, compared byte for byte, so two keys are never confused.
  • Iterate keys or recordskeys(store) and iterateStart/iterateNext.
  • Many stores per JVMName each store; size it up front or let it grow.

Zero-copy reads

  • RecordViewA read-only view of a record's bytes where they sit, with no copy onto the heap.
  • Views that know they're staleEach read checks the record's version stamp before and after, and throws StaleViewException if it changed.
  • Lock-free view readsA seqlock check, so readers never block a writer.
  • Parallel readersLookups share a read lock; writes take the write lock.

Documents & graphs

  • Document collectionsHybridDB stores Documents with insert, find, update, delete and full scans.
  • Secondary indexesO(1) lookups by field value, such as every employee in a department.
  • Graph databaseLabeled nodes and edges with properties, neighbors, BFS, DFS and shortest path.

Engine & observability

  • Open-addressing index13-byte entries, tombstones on delete, doubling at 50% load for O(n) amortized rehashing.
  • Off-heap free listsDeleted space is reused or split by 531 size classes from a fixed 4,248-byte table. Churn adds nothing to the heap.
  • Its own memory accountinggetAllocatedBytes, getTotalAllocatedBytes and getTotalMappedBytes, plus collisions, index load and timings.
Quick start

From clone to off-heap in three steps

NoHeapDB builds with Maven and needs Java 26 or later. There are no runtime dependencies.

  1. Build and install

    Clone the repo and install the JAR into your local Maven repository.

    git clone https://github.com/ericjbruno/NoHeapDB
    cd NoHeapDB
    mvn install -DskipTests
  2. Add the dependency

    <dependency>
      <groupId>com.alluretechnology</groupId>
      <artifactId>NoHeapDB</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
  3. Create a store and use it

    Pick an example on the right. Every store is named, so one NoHeapDB can hold many.

KeyValue.java
NoHeapDB db = new NoHeapDB();

// A 1 GB in-memory store; it adds chunks as it grows
db.createStore("cache", DataStore.Storage.IN_MEMORY, 1024);

db.putString("cache", "user:42", "Alice");
db.putInteger("cache", "visits:42", 17);

String name = db.getString("cache", "user:42");     // "Alice"
Integer visits = db.getInteger("cache", "visits:42"); // 17

db.remove("cache", "visits:42");

for (Iterator<String> keys = db.keys("cache"); keys.hasNext(); ) {
    System.out.println(keys.next());
}

db.deleteStore("cache");   // frees all of its memory now

The full API, including putObject, iteration and statistics, is in the README.

How it works

A journal of records and a hash index, both off-heap

Records are written back to back in the journal. A fixed-width hash index maps each key to a record's location.

A 32-byte record written by putInteger(store, "user:42", 1000): a 16-byte header, then the key and payload, padded to a multiple of 8 so the version stamp of every record stays 8-byte aligned.

  1. Hash to a bucket

    The key's hashcode picks a 13-byte bucket holding a length tag, the hashcode and a packed location: a 16-bit chunk number and a 48-bit offset.

  2. Follow the location

    Linear probing skips other keys and tombstones until a bucket's tag and hashcode match, then jumps straight to that chunk and offset.

  3. Confirm the key

    The key stored in the record is compared byte for byte. Only an exact match returns the value, or a zero-copy view of it.

Numbers

Measured, with every value checked

Each run reads back and verifies what it wrote. Wrong values are counted, not just missing ones.

GC events during 90 s of reads

10 GB of 64 KB records, -Xmx8m, G1. Bars drawn to scale.

Copying reads with getString()62,332
Zero-copy reads with RecordView412

Every collection was a young-generation pause, median 0.08 ms. No full or old-generation collections ran. Average read time fell from 12.6 µs to 1.0 µs.

20 million records, 32 MB heap

Short string records, one thread, average of two runs.

KeysWrites / sReads / sWrong values
Sequential7.9 M12.1 M0
Random UUIDs4.5 M5.9 M0

As a persisted store, the 10 GB test closed in 2.0 s, reopened in 0.9 s with the files still in the page cache, and read at 23,400 records per second.

Know before you adopt

What NoHeapDB doesn't do yet

No write-ahead log

Persisted data survives a process crash, because the OS still writes mapped pages out. A machine crash or power loss can lose writes not yet on disk.

Read scaling stops at two threads

Reads share a ReentrantReadWriteLock, whose counter becomes the bottleneck past two reader threads. Optimistic reads with StampedLock are planned.

Java 26 or later

The build targets Java 26. The FFM API it relies on is final from Java 22.

Copying reads still allocate

getString() returns a new String. For large values, read through a RecordView instead.