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 -DskipTestsNoHeapDBLargeDataTest: 10 GB of 64 KB records, 90 s of random reads, G1, every byte checked.-Xmx32m, one threadMeasured on the project's own test programs. Your numbers depend on record size, key shape and hardware.
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 lives | Java heap | Another process | Your process, off-heap |
| GC work grows with data | Yes | No | No |
| Cost of a read | Pointer chase | Round trip and deserialization | Direct memory read; zero-copy views |
| Survives a restart | No | If configured | Optional, as mapped files |
| Extra to run | Nothing | A server | Nothing |
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
MemorySegmentchunks 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 disk
IN_MEMORYstores, orPERSISTEDstores 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 gets
String,Integer,Long,Short,Float,Double,charand anySerializableobject. - Exact key matchingKeys up to 32,767 bytes, compared byte for byte, so two keys are never confused.
- Iterate keys or records
keys(store)anditerateStart/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
StaleViewExceptionif 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 collections
HybridDBstoresDocuments 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 accounting
getAllocatedBytes,getTotalAllocatedBytesandgetTotalMappedBytes, plus collisions, index load and timings.
From clone to off-heap in three steps
NoHeapDB builds with Maven and needs Java 26 or later. There are no runtime dependencies.
-
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 -
Add the dependency
<dependency> <groupId>com.alluretechnology</groupId> <artifactId>NoHeapDB</artifactId> <version>1.0-SNAPSHOT</version> </dependency> -
Create a store and use it
Pick an example on the right. Every store is named, so one
NoHeapDBcan hold many.
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
// A view of the stored bytes: nothing is copied onto the heap
RecordView view = db.getView("cache", "user:42");
int length = view.length();
byte first = view.get(0);
// read() hands your code a read-only ByteBuffer, then checks
// the record's version stamp again before returning
boolean startsWithA = view.read(bytes -> bytes.get(0) == 'A');
db.remove("cache", "user:42");
view.get(0); // throws StaleViewException: the record is gone
NoHeapDB db = new NoHeapDB("/data/myapp/offheap"); // default: ~/JavaOffHeap
db.createStore("orders", DataStore.Storage.PERSISTED, 1024);
db.putString("orders", "order:1001", "{\"total\": 129.95}");
db.closeStore("orders"); // flush and unmap; the files stay
// Later, or in a new JVM: the same call reopens the files
db.createStore("orders", DataStore.Storage.PERSISTED, 1024);
db.getString("orders", "order:1001");
// NoHeapDB reports its own memory use
long bytes = db.getAllocatedBytes("orders");
long mapped = NoHeapDB.getTotalMappedBytes();
HybridDB db = new HybridDB();
NoHeapCollection employees = db.createCollection("employees");
employees.createIndex("department"); // O(1) lookups by department
String id = employees.insert(
Document.create()
.set("name", "Alice Martin")
.set("department", "Engineering")
.set("salary", 135000));
List<Document> engineers = employees.find("department", "Engineering");
Document alice = employees.findById(id);
alice.set("salary", 142000);
employees.update(alice);
db.dropCollection("employees");
HybridDB db = new HybridDB();
NoHeapGraph graph = db.createGraph("accounts");
String alice = graph.addNode("Person", Map.of("name", "Alice"));
String bob = graph.addNode("Person", Map.of("name", "Bob"));
String acct = graph.addNode("Account", Map.of("iban", "DE89 3704"));
graph.addEdge(alice, acct, "OWNS", Map.of());
graph.addEdge(bob, acct, "SENT_TO", Map.of("amount", "9500"));
List<Node> neighbors = graph.getNeighbors(alice);
List<Node> reachable = graph.bfs(alice);
List<Node> path = graph.shortestPath(bob, alice);
db.dropGraph("accounts");
The full API, including putObject, iteration and statistics, is in the README.
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.
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.
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.
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.
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.
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.
| Keys | Writes / s | Reads / s | Wrong values |
|---|---|---|---|
| Sequential | 7.9 M | 12.1 M | 0 |
| Random UUIDs | 4.5 M | 5.9 M | 0 |
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.
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.
See it running
Start with the repository, then look at how a real server and the test programs use it.
ericjbruno/NoHeapDB
The engine, the document and graph layers, 99 correctness, concurrency and stress tests, and the figures behind the design.
github.com/ericjbruno/NoHeapDBMessageServer
A Micronaut pub/sub and queue server that keeps message bodies off-heap in a NoHeapDB store, in memory or persisted so queues survive a restart.
MessageServer / persist / MessageNoHeapDB.java Sample applicationEmployee directory
The document store end to end: a collection with a secondary index, inserts, indexed and full-scan finds, updates, deletes and iteration.
noheapdb / NoSQLExample.java Runnable test10 GB with an 8 MB heap
Loads large records off-heap and reads them back at random, printing heap, GC and throughput every 15 seconds. Run ./run-largedata-test.sh.
Delete and update churn
Tens of millions of deletes, updates and re-adds under an 8 MB heap, showing free-space reuse with a flat live heap. Run ./run-churn-test.sh.