Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/docs/architecture-overview.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ When the VM cannot be recognised, `UnknownJvmStringHash` provides a defensive fa
* `net.openhft.hashing.Primitives` houses byte-order normalisation helpers and unsigned conversions so algorithms can expect canonical little-endian operands even on big-endian hardware (`Primitives.java`).
* `net.openhft.hashing.Maths` provides low-level arithmetic helpers such as `unsignedLongMulXorFold` used by wyHash and XXH3 for 128-bit cross-products (`Maths.java`).
* Tests under `src/test/java/net/openhft/hashing` validate the API contract across arrays, primitives, buffers, and custom access strategies, and serve as reference snippets for typical `Access` usage.

== Backend selection

`RuntimeSupport` caches the JVM feature version; `HeapAccess` then chooses `UnsafeAccess` (JDK < 25) or `SafeHeapAccess` (JDK 25+) once at class initialisation. See ADR-007 in the decision log.
19 changes: 19 additions & 0 deletions src/main/docs/decision-log.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,22 @@ Chronicle Software
* **Positive:** Supports >64-bit hashes without allocating an Object wrapper (like `BigInteger` or a custom class).
* **Negative:** The `long[]` result array must be managed/reused by the caller to maintain the "zero-allocation" promise.
|===

== ADR-007: Unsafe-free heap access on JDK 25+

[cols="1,3"]
|===
| Status | Accepted
| Date | 2025-11-01
| Context | `sun.misc.Unsafe` memory access is being phased out. JDK 25+ should never instantiate it, while JDK 8-24 (HotSpot, IBM Semeru/OpenJ9, AIX ppc64, Linux ppc64le) keep today's fast path. The feature version is constant for the lifetime of a JVM.
| Decision |
* `RuntimeSupport` caches the feature version once (static final); `useUnsafeAccess()` is true below 25. The system property `net.openhft.hashing.useUnsafe=false` forces the safe backend on older JDKs for testing.
* `HeapAccess` selects the array backend once: `UnsafeAccess` (real base offsets) or `SafeHeapAccess` (base offset 0, native-order byte view built on array indexing, no allocation).
* `UnsafeAccess` refuses to initialise when the safe backend is selected, so Unsafe is never instantiated on JDK 25+.
* On JDK 25+, strings use `UnknownJvmStringHash` (`CharSequence` reads) and `ByteBuffer` uses `ByteBufferAccess`.
* `hashMemory` uses `ForeignRawAccess`, a direct translation of `Unsafe.getX(null, address)` to the FFM API (JDK 22+, via method handles so the Java 8 build is unchanged). If FFM is unavailable it throws `UnsupportedOperationException`.
* `Access.unsafe()` throws on the safe backend: object plus field-offset access cannot be expressed without Unsafe.
| Consequences |
* **Positive:** Hash values are identical on every backend; zero allocation is retained; no behaviour change below JDK 25.
* **Negative:** The safe backend composes wide reads from bytes and is slower; raw-memory hashing is unavailable on JDK 25+.
|===
8 changes: 8 additions & 0 deletions src/main/docs/unsafe-and-platform-notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ Hash outputs therefore match across architectures, but big-endian CPUs incur add
This ensures correctness at the cost of potential performance; review the decision log before altering the detection logic (`Util.java:29-63`).
* Passing invalid pointers to `hashMemory` or misreporting `Access` byte order is undefined and can crash the JVM.
Always validate foreign memory addresses and keep access strategies consistent with the underlying storage layout.

=== JDK 25+ and the Unsafe-free backend

* `RuntimeSupport.useUnsafeAccess()` is false on JDK 25+ (and when `-Dnet.openhft.hashing.useUnsafe=false` is set); `UnsafeAccess` then throws `IllegalStateException` if touched, so `sun.misc.Unsafe` is never instantiated.
* `HeapAccess` supplies array base offsets and the `Access` used by `LongHashFunction`, `LongTupleHashFunction` and `XXH3`; the safe variant (`SafeHeapAccess`) uses base offset 0 and native byte order, so big-endian hosts (AIX ppc64) still normalise through `Access.byteOrder`.
* `hashMemory` uses `ForeignRawAccess` (FFM, JDK 22+). It calls the restricted `MemorySegment.reinterpret`; JDK 24+ warns once unless `--enable-native-access=ALL-UNNAMED` is set. Without FFM it throws `UnsupportedOperationException`.
* `Access.unsafe()` throws `UnsupportedOperationException` on this backend; direct `ByteBuffer`s are hashed through `ByteBufferAccess`.
* Strings use `UnknownJvmStringHash`; this applies equally to HotSpot and IBM Semeru/OpenJ9.
3 changes: 3 additions & 0 deletions src/main/java/net/openhft/hashing/Access.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,12 @@ public abstract class Access<T> {
*
* @param <T> the type of objects to access
* @return the unsafe memory {@code Access}
* @throws UnsupportedOperationException on JDK 25+ (or with
* {@code -Dnet.openhft.hashing.useUnsafe=false}), where Unsafe is never instantiated
*/
@SuppressWarnings("unchecked")
public static <T> Access<T> unsafe() {
HeapAccess.requireUnsafe();
return (Access<T>) UnsafeAccess.INSTANCE;
}

Expand Down
151 changes: 151 additions & 0 deletions src/main/java/net/openhft/hashing/ForeignRawAccess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
*/
package net.openhft.hashing;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.nio.ByteOrder;

/**
* Raw native-address {@link Access} built on the Foreign Function and Memory API (final in
* JDK 22), used instead of {@code sun.misc.Unsafe} where Unsafe is not used.
*
* <p>The API is reached through method handles so that the library still compiles for Java 8 and
* ships as a single artifact. The handles are bound to one process-wide segment spanning the whole
* address space ({@code MemorySegment.NULL.reinterpret(Long.MAX_VALUE)}), so the "offset" of an
* access is the absolute address, exactly like {@code Unsafe.getLong(null, address)}. The handles
* are static finals and so are constant-folded by the JIT; reads allocate nothing.
*
* <p>{@code reinterpret} is a restricted method: JDK 24+ prints a one-off warning unless
* {@code --enable-native-access=ALL-UNNAMED} is given. If the API is missing or refused,
* {@link #INSTANCE} is {@code null} and raw memory stays unsupported.
*
* <p>The {@code input} argument is ignored and must be {@code null}; this class is not exposed
* publicly because object plus field-offset access (see {@link Access#unsafe()}) cannot be
* expressed with it. Addresses at or above {@code 2^63} are not supported.
*/
final class ForeignRawAccess extends Access<Object> {
static final ForeignRawAccess INSTANCE;

private static final MethodHandle GET_LONG;
private static final MethodHandle GET_INT;
private static final MethodHandle GET_SHORT;
private static final MethodHandle GET_BYTE;
private static final Access<Object> NON_NATIVE;

static {
MethodHandle l = null;
MethodHandle i = null;
MethodHandle s = null;
MethodHandle b = null;
if (RuntimeSupport.isAtLeast(22)) {
try {
final Class<?> segmentClass = Class.forName("java.lang.foreign.MemorySegment");
final Class<?> layouts = Class.forName("java.lang.foreign.ValueLayout");
final Object global = segmentClass.getMethod("reinterpret", long.class)
.invoke(segmentClass.getField("NULL").get(null), Long.MAX_VALUE);
l = bind(segmentClass, layouts, global, "JAVA_LONG_UNALIGNED", "OfLong", long.class);
i = bind(segmentClass, layouts, global, "JAVA_INT_UNALIGNED", "OfInt", int.class);
s = bind(segmentClass, layouts, global, "JAVA_SHORT_UNALIGNED", "OfShort", short.class);
b = bind(segmentClass, layouts, global, "JAVA_BYTE", "OfByte", byte.class);
} catch (final Throwable ignore) {
l = null;
}
}
final boolean ok = l != null && i != null && s != null && b != null;
GET_LONG = ok ? l : null;
GET_INT = ok ? i : null;
GET_SHORT = ok ? s : null;
GET_BYTE = ok ? b : null;
INSTANCE = ok ? new ForeignRawAccess() : null;
NON_NATIVE = ok ? Access.newDefaultReverseAccess(INSTANCE) : null;
}

private ForeignRawAccess() {}

/** Returns a {@code (long address) -> value} handle reading with the given native layout. */
private static MethodHandle bind(final Class<?> segmentClass, final Class<?> layouts,
final Object global, final String layoutField,
final String layoutType, final Class<?> valueType)
throws ReflectiveOperationException {
final Object layout = layouts.getField(layoutField).get(null);
final Class<?> layoutClass = Class.forName("java.lang.foreign.ValueLayout$" + layoutType);
final Method get = segmentClass.getMethod("get", layoutClass, long.class);
final MethodHandle handle = MethodHandles.lookup().unreflect(get);
return MethodHandles.insertArguments(handle, 0, global, layout)
.asType(MethodType.methodType(valueType, long.class));
}

@Override
public long getLong(final Object input, final long offset) {
try {
return (long) GET_LONG.invokeExact(offset);
} catch (final Throwable t) {
throw rethrow(t);
}
}

@Override
public long getUnsignedInt(final Object input, final long offset) {
return Primitives.unsignedInt(getInt(input, offset));
}

@Override
public int getInt(final Object input, final long offset) {
try {
return (int) GET_INT.invokeExact(offset);
} catch (final Throwable t) {
throw rethrow(t);
}
}

@Override
public int getUnsignedShort(final Object input, final long offset) {
return Primitives.unsignedShort(getShort(input, offset));
}

@Override
public int getShort(final Object input, final long offset) {
try {
return (short) GET_SHORT.invokeExact(offset);
} catch (final Throwable t) {
throw rethrow(t);
}
}

@Override
public int getUnsignedByte(final Object input, final long offset) {
return Primitives.unsignedByte(getByte(input, offset));
}

@Override
public int getByte(final Object input, final long offset) {
try {
return (byte) GET_BYTE.invokeExact(offset);
} catch (final Throwable t) {
throw rethrow(t);
}
}

private static RuntimeException rethrow(final Throwable t) {
if (t instanceof RuntimeException) {
return (RuntimeException) t;
} else if (t instanceof Error) {
throw (Error) t;
}
return new IllegalStateException(t);
}

@Override
public ByteOrder byteOrder(final Object input) {
return ByteOrder.nativeOrder();
}

@Override
protected Access<Object> reverseAccess() {
return NON_NATIVE;
}
}
81 changes: 81 additions & 0 deletions src/main/java/net/openhft/hashing/HeapAccess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
*/
package net.openhft.hashing;

/**
* One-time selection of the array access backend.
*
* <p>When {@link RuntimeSupport#useUnsafeAccess()} is false (JDK 25+ or forced by property) this
* class never touches {@link UnsafeAccess}, so {@code sun.misc.Unsafe} is never instantiated.
* Raw-memory and {@code DirectBuffer} fast paths are only available with Unsafe.
*/
final class HeapAccess {
static final boolean UNSAFE_ENABLED = RuntimeSupport.useUnsafeAccess();

static final Access<Object> ACCESS;

/** Access for absolute native addresses, or {@code null} if raw memory is unsupported. */
static final Access<Object> RAW_MEMORY_ACCESS;
static final boolean RAW_MEMORY_AVAILABLE;

static final long BOOLEAN_BASE;
static final long BYTE_BASE;
static final long CHAR_BASE;
static final long SHORT_BASE;
static final long INT_BASE;
static final long LONG_BASE;

static final byte TRUE_BYTE_VALUE;
static final byte FALSE_BYTE_VALUE;

static {
if (UNSAFE_ENABLED) {
ACCESS = UnsafeAccess.INSTANCE;
RAW_MEMORY_ACCESS = UnsafeAccess.INSTANCE;
BOOLEAN_BASE = UnsafeAccess.BOOLEAN_BASE;
BYTE_BASE = UnsafeAccess.BYTE_BASE;
CHAR_BASE = UnsafeAccess.CHAR_BASE;
SHORT_BASE = UnsafeAccess.SHORT_BASE;
INT_BASE = UnsafeAccess.INT_BASE;
LONG_BASE = UnsafeAccess.LONG_BASE;
TRUE_BYTE_VALUE = UnsafeAccess.TRUE_BYTE_VALUE;
FALSE_BYTE_VALUE = UnsafeAccess.FALSE_BYTE_VALUE;
} else {
ACCESS = SafeHeapAccess.INSTANCE;
RAW_MEMORY_ACCESS = ForeignRawAccess.INSTANCE;
BOOLEAN_BASE = 0L;
BYTE_BASE = 0L;
CHAR_BASE = 0L;
SHORT_BASE = 0L;
INT_BASE = 0L;
LONG_BASE = 0L;
TRUE_BYTE_VALUE = 1;
FALSE_BYTE_VALUE = 0;
}
}

static {
RAW_MEMORY_AVAILABLE = RAW_MEMORY_ACCESS != null;
}

private HeapAccess() {}

/** Returns the access for {@code hashMemory}; fails fast if no raw memory backend exists. */
static Access<Object> rawMemoryAccess() {
if (RAW_MEMORY_ACCESS == null) {
throw new UnsupportedOperationException(
"Raw memory access needs sun.misc.Unsafe (JDK < 25) or the FFM API (JDK 22+), "
+ "neither is usable on this JVM");
}
return RAW_MEMORY_ACCESS;
}

/** Guards {@link Access#unsafe()}: object plus field-offset access needs real Unsafe. */
static void requireUnsafe() {
if (!UNSAFE_ENABLED) {
throw new UnsupportedOperationException(
"Access.unsafe() requires sun.misc.Unsafe, which is not used on this JVM");
}
}
}
14 changes: 11 additions & 3 deletions src/main/java/net/openhft/hashing/LongHashFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.nio.ByteOrder;

import static net.openhft.hashing.CharSequenceAccess.nativeCharSequenceAccess;
import static net.openhft.hashing.UnsafeAccess.*;
import static net.openhft.hashing.HeapAccess.*;
import static net.openhft.hashing.Util.VALID_STRING_HASH;
import static net.openhft.hashing.Util.checkArrayOffs;

Expand Down Expand Up @@ -499,8 +499,13 @@ protected LongHashFunction() {
*/
public abstract <T> long hash(T input, Access<T> access, long off, long len);

/*
* Historical name: this hashes through HeapAccess.ACCESS, which is Unsafe based only below
* JDK 25. On JDK 25+ (or useUnsafe=false) it is the Unsafe-free SafeHeapAccess; only the
* null-input (raw address) case needs real Unsafe and then fails fast.
*/
private long unsafeHash(Object input, long off, long len) {
return hash(input, UnsafeAccess.INSTANCE, off, len);
return hash(input, input == null ? HeapAccess.rawMemoryAccess() : HeapAccess.ACCESS, off, len);
}

/**
Expand Down Expand Up @@ -606,7 +611,7 @@ public long hashBytes(@NotNull ByteBuffer input, int off, int len) {
private long hashByteBuffer(@NotNull ByteBuffer input, int off, int len) {
if (input.hasArray()) {
return unsafeHash(input.array(), BYTE_BASE + input.arrayOffset() + off, len);
} else if (input instanceof DirectBuffer) {
} else if (UNSAFE_ENABLED && input instanceof DirectBuffer) {
return unsafeHash(null, ((DirectBuffer) input).address() + off, len);
} else {
return hash(input, ByteBufferAccess.INSTANCE, off, len);
Expand All @@ -622,6 +627,9 @@ private long hashByteBuffer(@NotNull ByteBuffer input, int off, int len) {
* @param address the address of the first byte to hash
* @param len length of the byte sequence to hash
* @return hash code for the specified byte sequence
* @throws UnsupportedOperationException on JDK 25+ (or with
* {@code -Dnet.openhft.hashing.useUnsafe=false}); hash a direct {@link ByteBuffer}
* via {@link #hashBytes(ByteBuffer)} instead
*/
public long hashMemory(long address, long len) {
return unsafeHash(null, address, len);
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/net/openhft/hashing/LongTupleHashFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import java.nio.ByteOrder;

import static net.openhft.hashing.CharSequenceAccess.nativeCharSequenceAccess;
import static net.openhft.hashing.UnsafeAccess.*;
import static net.openhft.hashing.HeapAccess.*;
import static net.openhft.hashing.Util.*;

/**
Expand Down Expand Up @@ -601,6 +601,8 @@ public long[] hashBytes(final ByteBuffer input, final int off, final int len) {
* @throws IllegalArgumentException if {@code result.length < newResultArray().length}
* @throws IllegalArgumentException if {@code off < 0} or {@code off + len > input.length}
* or {@code len < 0}
* @throws UnsupportedOperationException on JDK 25+ (or with
* {@code -Dnet.openhft.hashing.useUnsafe=false}), where raw memory is unavailable
*/
public void hashMemory(final long address, final long len, final long[] result) {
unsafeHash(this, null, address, len, result);
Expand Down Expand Up @@ -989,22 +991,24 @@ public long[] hashLongs(final long[] input, final int off, final int len) {
// Internal helper
//
@NotNull
private static final Access<Object> OBJECT_ACCESS = UnsafeAccess.INSTANCE;
// Unsafe based below JDK 25, SafeHeapAccess otherwise.
private static final Access<Object> OBJECT_ACCESS = HeapAccess.ACCESS;
@NotNull
private static final Access<CharSequence> CHAR_SEQ_ACCESS = nativeCharSequenceAccess();
@NotNull
private static final Access<ByteBuffer> BYTE_BUF_ACCESS = ByteBufferAccess.INSTANCE;

// Historical name, see LongHashFunction#unsafeHash: Unsafe is only used below JDK 25.
private static void unsafeHash(final LongTupleHashFunction f, @Nullable final Object input,
final long off, final long len, final long[] result) {
f.hash(input, OBJECT_ACCESS, off, len, result);
f.hash(input, input == null ? HeapAccess.rawMemoryAccess() : OBJECT_ACCESS, off, len, result);
}

private static void hashByteBuffer(final LongTupleHashFunction f, final ByteBuffer input,
final int off, final int len, final long[] result) {
if (input.hasArray()) {
unsafeHash(f, input.array(), BYTE_BASE + input.arrayOffset() + off, len, result);
} else if (input instanceof DirectBuffer) {
} else if (UNSAFE_ENABLED && input instanceof DirectBuffer) {
unsafeHash(f, null, ((DirectBuffer) input).address() + off, len, result);
} else {
f.hash(input, BYTE_BUF_ACCESS, off, len, result);
Expand Down
Loading