A single-threaded, off-heap hash map implementation in Java that fully implements the java.util.Map contract. This project utilizes Java's Foreign Function & Memory (FFM) API to manage data structures directly in native memory, bypassing the Java Garbage Collector (GC) for the underlying table nodes.
- Installation
- Memory Lifecycle & AutoCloseable
- Implementation Details
- Performance Optimizations
- Testing Strategy
- Running the Project
- Releasing and Versioning
Off-Heap Mapish is published to GitHub Packages. To consume it in your project, add the following to your build.gradle.kts:
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/bhf/mapish")
credentials {
username = System.getenv("GITHUB_ACTOR") // Your GitHub username
password = System.getenv("GITHUB_TOKEN") // A PAT with `read:packages` scope
}
}
}
dependencies {
implementation("com.bhf.mapish:off-heap-map:1.0-SNAPSHOT")
}By default, an OffHeapMap is created using Arena.ofAuto(). This delegates the cleanup of native memory to the Java Garbage Collector, allowing it to behave somewhat like a standard on-heap object and safely avoid accidental use-after-free errors.
OffHeapMap<String, String> map = new OffHeapMap<>(new StringSerializer(100), new StringSerializer(100), 2_000_000);
map.put("key", "value");
// No explicit closing required; memory is freed when the map is garbage collected.However, if you are allocating very large maps and need strict, deterministic control over the native memory release (to prevent exhausting system memory resources before the GC runs), you can instantiate a confined map:
try (OffHeapMap<String, String> map = OffHeapMap.createConfined(new StringSerializer(100), new StringSerializer(100), 2_000_000)) {
map.put("key", "value");
// Work with the map
} // The native memory is immediately unmapped and released to the OS hereConfined maps implement AutoCloseable and immediately drop the underlying memory allocations upon closing. Any attempt to read from or write to the map after it has been closed will safely throw an IllegalStateException.
Instead of relying on arrays of Java Object references (which live on the heap and are tracked by the GC), OffHeapMap allocates a single, contiguous off-heap MemorySegment to store all keys, values, and structural metadata.
- FFM API: Memory is managed directly using
Arena.ofAuto()andMemorySegment. - Generic Serializers: Because the map stores raw bytes, generic
<K, V>types are supported via a customSerializer<T>interface. The map includes built-in serializers for primitives and common types (String,Integer,Long,Double,Float,Short,Byte,Boolean,Character) encapsulated in thecom.bhf.mapish.serializerspackage. - Collision Resolution: Instead of the separate chaining and Red-Black trees used by the standard
java.util.HashMap, this map uses Open Addressing with Linear Probing. This avoids complex pointer management, significantly reduces memory overhead, and maximizes CPU cache locality by scanning adjacent memory blocks. - Memory Layout: Each entry in the map is stored sequentially in a fixed-size chunk calculated dynamically at startup:
[1 byte Status] [Padding] [4 byte Cached Hash] [Key Bytes] [Value Bytes]
To mitigate the heavy costs of serializing and deserializing data back and forth between the JVM heap and native memory, we implemented several key performance tricks:
- Hash Caching: The 32-bit
hashCode()of every key is cached locally within the entry metadata layout. When the map linearly probes through a collision chain, it checks for integer equivalence first. This allows the map to skip complex deserialization on hash mismatches entirely. - Zero-Deserialization Comparisons: The
Serializerinterface implements a specializedequals(T obj, MemorySegment segment, long offset)method. For types likeString, instead of allocating a newbyte[]and extracting a UTF-8Stringonto the heap just to check equality, we utilizeMemorySegment.mismatch()to execute blazing-fast binary sequence comparisons directly against the hardware vector logic. - Pre-sizing / Capacity Allocation: Re-allocating massive contiguous memory segments off-heap can be incredibly expensive and trigger the JVM's
OutOfMemoryErroron native buffer reservations. Pre-sizing the map via the constructor avoids O(N) array copying and allows million-entry insertions instantly.
Validating custom data structures requires rigorous boundaries testing.
- Contract Verification: We utilize Google's
guava-testlibviaMapTestSuiteBuilder. This dynamically generates hundreds of permutations and structural edge-case tests to strictly verify thatOffHeapMapconforms 100% perfectly to the standardjava.util.Mapinterface contracts (handling null boundaries,entrySet()iterator removals, deephashCode()constraints, etc.). - Benchmarking: Throughput is measured using Java Microbenchmark Harness (JMH). The benchmark suite (
src/jmh/java/com/bhf/mapish/benchmarks/) includes separate, encapsulated benchmarks for each primitive serializer (String, Integer, Double, etc.). It compares operations (put,get,containsKey) against Yahoo'sOakMap(another off-heap map implementation) over 100 and 10,000 element scale runs to observe scaling thresholds, memory boundaries, and throughput operations per millisecond. - Component Testing: Comprehensive JUnit tests ensure stability across the isolated serializers and
AutoCloseablememory lifecycles.
Run the Guava Unit Test Suite:
./gradlew testRun the JMH Benchmarks:
./gradlew jmhNote: The entire JMH test suite is configured to output metrics directly in JSON format (found in build/results/jmh/results.json) making it easy for machine analysis and visualization pipelines.
This project uses a semantic version properties file to manage artifact releases via a custom Gradle task.
Bumping the Version:
To increment the version in gradle.properties, utilize the custom bumpVersion task:
# Bump patch version (e.g. 1.0.0-SNAPSHOT -> 1.0.1-SNAPSHOT)
./gradlew bumpVersion -Ptype=patch
# Bump minor version (e.g. 1.0.1-SNAPSHOT -> 1.1.0-SNAPSHOT)
./gradlew bumpVersion -Ptype=minor
# Bump major version (e.g. 1.1.0-SNAPSHOT -> 2.0.0-SNAPSHOT)
./gradlew bumpVersion -Ptype=majorDropping the Snapshot Qualifier:
When you are ready to prepare a formal production release, add -Psnapshot=false to drop the suffix (e.g. 1.1.0-SNAPSHOT becomes 1.1.0).
# Finalize the current snapshot into a hard release version
./gradlew release -Psnapshot=false
# Bump the minor version AND make it a non-snapshot
./gradlew release -Ptype=minor -Psnapshot=falseAutomated Publishing (Recommended):
You can trigger the entire release process from the command line. This combined task bumps the version, commits the modification, pushes your branch to GitHub, and automatically utilizes the GitHub CLI (gh) to spawn a formal release (which triggers the CI pipeline to publish the Packages):
# Finalizes, commits, pushes, and creates release vX.Y.Z over the API
./gradlew release -Psnapshot=false(Note: Automated releasing requires the GitHub CLI to be installed and authenticated locally).
Manual Publishing:
If you prefer not to use the automated gh CLI task, ensure that you have committed your version bump manually (./gradlew bumpVersion -Ptype=minor -Psnapshot=false). Then you can trigger the release directly on GitHub:
- Option 1: Navigate to the project's GitHub page and hit Create a new release.
- Option 2: Go to the GitHub repository Actions tab, select the Publish release to GitHub Packages workflow, and click Run workflow.