diff --git a/README.md b/README.md
index 7e52a8d..c0fbab2 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T
| [`kdf/`](kdf) | Key Derivation Function API in Java (JEP 510): HKDF Explained with Real Examples | `javax.crypto.KDF` final in JDK 25, `HKDF-SHA256/384/512`, extract vs expand, RFC 5869 vectors asserted, a hand-written HKDF and 300 generated cases compared with Python `cryptography` and `openssl kdf`, per-tenant keys with length-prefixed `info`, and the mistakes reproduced (same `info`, ambiguous concatenation, PIN through HKDF vs PBKDF2) |
| [`classfile-api/`](classfile-api) | The Java ClassFile API (JEP 484): Read, Generate and Transform Bytecode Without ASM | `java.lang.classfile` final since JDK 24: `parse` and `ClassModel` traversal, `ClassFile.build` with `defineClass` and `defineHiddenClass`, five deliberately broken classes (including swapped exception-table labels that `verify()` misses), timing inserted with `ClassTransform`/`MethodTransform`/`CodeTransform`, the same three jobs in ASM 9.11.1 asserted equal, class-file version limits, a JDK 24/25/26 API diff and a look inside `spring-core` 7.0.10 |
| [`jextract/`](jextract) | Calling C from Java with jextract and the FFM API: A Real Library, No JNI | jextract (early-access build for JDK 25) bindings for SQLite 3.45.1, the four arena kinds used wrongly on purpose, `C_POINTER` vs `ADDRESS` (zero-length pointers, and a `SIGSEGV` from `getString` on a NULL pointer), `sqlite3_open` out-parameter, return codes to exceptions, a `sqlite3_exec` upcall (and the JVM exit when it throws), the JDK 25 native-access warning and `--illegal-native-access=deny`, and a JMH comparison of FFM, hand-written JNI and plain Java |
+| [`unsafe-migration/`](unsafe-migration) | sun.misc.Unsafe Memory Access Is Going Away: Find and Fix It in Your Dependencies (JEP 471 / 498) | what `--sun-misc-unsafe-memory-access` allow/warn/debug/deny do on JDK 25 (and 21, 26, 27), `javac` removal warnings, jdeps and a bytecode scan of 17 real jars, Netty and Guava at run time, a CAS field to `VarHandle`, an off-heap array to `MemorySegment`, a before/after equivalence check |
## Captured output (`docs/output/`)
diff --git a/unsafe-migration/README.md b/unsafe-migration/README.md
new file mode 100644
index 0000000..a69c6cf
--- /dev/null
+++ b/unsafe-migration/README.md
@@ -0,0 +1,24 @@
+# unsafe-migration — leaving sun.misc.Unsafe (JEP 471 / JEP 498)
+
+Companion code for the ankurm.com article **sun.misc.Unsafe Memory Access Is Going Away: Find and Fix It in Your Dependencies**
+(). The explanation lives in the article; this folder holds the sources and the transcripts they produced.
+
+```bash
+JDK25=/path/to/jdk-25 JDK21=/path/to/jdk-21 [JDK26=/path JDK27=/path] ./run.sh # regenerates output/*.txt (a few minutes; steps 06-08 need Maven Central)
+```
+
+Tested on Temurin 25.0.5+8 (the baseline), Ubuntu OpenJDK 21.0.12.1 (the "before"), Temurin 26.0.2.1+1 and 27.0.1.0.2+1 (default-behaviour check only). Plain `javac`/`java`, no Maven.
+
+| Path | What it is |
+|---|---|
+| `src/UnsafeFieldCounter.java`, `VarHandleFieldCounter.java`, `FieldCounter.java` | a CAS field: Unsafe before, VarHandle after |
+| `src/UnsafeOffHeapLongs.java`, `FfmOffHeapLongs.java`, `OffHeapLongs.java` | an off-heap `long` array: `allocateMemory` before, `Arena`/`MemorySegment` after |
+| `src/UnsafeBytes.java`, `ViewBytes.java` | long access inside a `byte[]`: bimodal `getLong(Object,long)` before, byte-array-view VarHandle after |
+| `src/Equivalence.java` | the before/after check (170 checks; 67 with `--new-only` under `deny`) |
+| `broken/Probe.java` | the smallest Unsafe memory access; what every flag value does to it |
+| `tools/ScanJar.java` | bytecode scan for removal-deprecated Unsafe calls (java.lang.classfile, JDK 24+) |
+| `tools/NettyProbe.java`, `GuavaProbe.java`, `ListUnsafe.java`, `Prop.java` | one real Netty buffer; three Guava features; the JDK's own method list; the flag as a system property |
+| `scripts/scan-libs.sh`, `scripts/run-all.sh` | download and scan 17 jars (sha1-checked; retries because the proxy here truncated transfers now and then); regenerate everything |
+| `output/01`..`12` | captured transcripts: methods per JDK, javac warnings, flag values, other JDKs, jdeps, library scan, Netty, equivalence, JFR, scan callers, Guava |
+
+Timing is not measured anywhere in this module; there is no benchmark.
diff --git a/unsafe-migration/broken/Probe.java b/unsafe-migration/broken/Probe.java
new file mode 100644
index 0000000..c3ebf41
--- /dev/null
+++ b/unsafe-migration/broken/Probe.java
@@ -0,0 +1,29 @@
+import sun.misc.Unsafe;
+
+/**
+ * Smallest possible Unsafe memory access: allocate 8 bytes off-heap, write a long, read it back, free it.
+ * Run it under each value of --sun-misc-unsafe-memory-access to see what the JVM does.
+ */
+public class Probe {
+ public static void main(String[] args) {
+ Unsafe u = UnsafeAccess.get();
+ System.out.println("got Unsafe instance");
+ try {
+ long addr = u.allocateMemory(8);
+ u.putLong(addr, 42L);
+ System.out.println("read back " + u.getLong(addr));
+ u.freeMemory(addr);
+ System.out.println("allocate/put/get/free all succeeded");
+ } catch (Throwable t) {
+ System.out.println("FAILED: " + t);
+ }
+ try {
+ long addr2 = u.allocateMemory(8);
+ u.putLong(addr2, 7L);
+ System.out.println("second use read " + u.getLong(addr2));
+ u.freeMemory(addr2);
+ } catch (Throwable t) {
+ System.out.println("second use FAILED: " + t);
+ }
+ }
+}
diff --git a/unsafe-migration/output/01-unsafe-methods-per-jdk.txt b/unsafe-migration/output/01-unsafe-methods-per-jdk.txt
new file mode 100644
index 0000000..1daf840
--- /dev/null
+++ b/unsafe-migration/output/01-unsafe-methods-per-jdk.txt
@@ -0,0 +1,119 @@
+== openjdk version "21.0.12.1" 2026-08-18
+java.version=21.0.12.1
+public methods declared by sun.misc.Unsafe: 88
+deprecated for removal: 2
+not deprecated for removal: 86
+ addressSize
+ allocateInstance
+ allocateMemory
+ arrayBaseOffset
+ arrayIndexScale
+ compareAndSwapInt
+ compareAndSwapLong
+ compareAndSwapObject
+ copyMemory
+ copyMemory
+ freeMemory
+ fullFence
+ getAddress
+ getAndAddInt
+ getAndAddLong
+ getAndSetInt
+ getAndSetLong
+ getAndSetObject
+ getBoolean
+ getBooleanVolatile
+ getByte
+ getByte
+ getByteVolatile
+ getChar
+ getChar
+ getCharVolatile
+ getDouble
+ getDouble
+ getDoubleVolatile
+ getFloat
+ getFloat
+ getFloatVolatile
+ getInt
+ getInt
+ getIntVolatile
+ getLoadAverage
+ getLong
+ getLong
+ getLongVolatile
+ getObject
+ getObjectVolatile
+ getShort
+ getShort
+ getShortVolatile
+ invokeCleaner
+ loadFence
+ objectFieldOffset (deprecated, not for removal)
+ pageSize
+ park
+ putAddress
+ putBoolean
+ putBooleanVolatile
+ putByte
+ putByte
+ putByteVolatile
+ putChar
+ putChar
+ putCharVolatile
+ putDouble
+ putDouble
+ putDoubleVolatile
+ putFloat
+ putFloat
+ putFloatVolatile
+ putInt
+ putInt
+ putIntVolatile
+ putLong
+ putLong
+ putLongVolatile
+ putObject
+ putObjectVolatile
+ putOrderedInt
+ putOrderedLong
+ putOrderedObject
+ putShort
+ putShort
+ putShortVolatile
+ reallocateMemory
+ setMemory
+ setMemory
+ staticFieldBase (deprecated, not for removal)
+ staticFieldOffset (deprecated, not for removal)
+ storeFence
+ throwException
+ unpark
+
+== openjdk version "25.0.5" 2026-10-20 LTS
+java.version=25.0.5
+public methods declared by sun.misc.Unsafe: 86
+deprecated for removal: 83
+not deprecated for removal: 3
+ allocateInstance
+ pageSize
+ throwException
+
+== openjdk version "26.0.2.1" 2026-08-18
+java.version=26.0.2.1
+public methods declared by sun.misc.Unsafe: 86
+deprecated for removal: 83
+not deprecated for removal: 3
+ allocateInstance
+ pageSize
+ throwException
+
+== openjdk version "27.0.1.0.2" 2026-10-21
+java.version=27.0.1.0.2
+public methods declared by sun.misc.Unsafe: 86
+deprecated for removal: 83
+not deprecated for removal: 3
+ allocateInstance
+ pageSize
+ throwException
+
diff --git a/unsafe-migration/output/02-compile-warnings.txt b/unsafe-migration/output/02-compile-warnings.txt
new file mode 100644
index 0000000..3978845
--- /dev/null
+++ b/unsafe-migration/output/02-compile-warnings.txt
@@ -0,0 +1,34 @@
+$ javac src/UnsafeAccess.java broken/Probe.java (openjdk version "25.0.5" 2026-10-20 LTS)
+src/UnsafeAccess.java:2: warning: Unsafe is internal proprietary API and may be removed in a future release
+broken/Probe.java:1: warning: Unsafe is internal proprietary API and may be removed in a future release
+src/UnsafeAccess.java:8: warning: Unsafe is internal proprietary API and may be removed in a future release
+src/UnsafeAccess.java:10: warning: Unsafe is internal proprietary API and may be removed in a future release
+src/UnsafeAccess.java:12: warning: Unsafe is internal proprietary API and may be removed in a future release
+broken/Probe.java:9: warning: Unsafe is internal proprietary API and may be removed in a future release
+broken/Probe.java:12: warning: [removal] allocateMemory(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:13: warning: [removal] putLong(long,long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:14: warning: [removal] getLong(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:15: warning: [removal] freeMemory(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:21: warning: [removal] allocateMemory(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:22: warning: [removal] putLong(long,long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:23: warning: [removal] getLong(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:24: warning: [removal] freeMemory(long) in Unsafe has been deprecated and marked for removal
+14 warnings
+
+$ javac src/UnsafeAccess.java broken/Probe.java (openjdk version "21.0.12.1" 2026-08-18)
+src/UnsafeAccess.java:2: warning: Unsafe is internal proprietary API and may be removed in a future release
+broken/Probe.java:1: warning: Unsafe is internal proprietary API and may be removed in a future release
+src/UnsafeAccess.java:8: warning: Unsafe is internal proprietary API and may be removed in a future release
+src/UnsafeAccess.java:10: warning: Unsafe is internal proprietary API and may be removed in a future release
+src/UnsafeAccess.java:12: warning: Unsafe is internal proprietary API and may be removed in a future release
+broken/Probe.java:9: warning: Unsafe is internal proprietary API and may be removed in a future release
+6 warnings
+
+$ javac -Xlint:removal -Werror src/UnsafeAccess.java broken/Probe.java (openjdk version "25.0.5" 2026-10-20 LTS)
+error: warnings found and -Werror specified
+broken/Probe.java:12: warning: [removal] allocateMemory(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:13: warning: [removal] putLong(long,long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:14: warning: [removal] getLong(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:15: warning: [removal] freeMemory(long) in Unsafe has been deprecated and marked for removal
+broken/Probe.java:21: warning: [removal] allocateMemory(long) in Unsafe has been deprecated and marked for removal
+exit=1
diff --git a/unsafe-migration/output/03-flag-values-jdk25.txt b/unsafe-migration/output/03-flag-values-jdk25.txt
new file mode 100644
index 0000000..74054e8
--- /dev/null
+++ b/unsafe-migration/output/03-flag-values-jdk25.txt
@@ -0,0 +1,64 @@
+$ java -cp build Probe (JDK 25 default)
+got Unsafe instance
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::allocateMemory has been called by Probe (file:/c25/)
+WARNING: Please consider reporting this to the maintainers of class Probe
+WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
+read back 42
+allocate/put/get/free all succeeded
+second use read 7
+
+$ java --sun-misc-unsafe-memory-access=allow -cp build Probe
+got Unsafe instance
+read back 42
+allocate/put/get/free all succeeded
+second use read 7
+
+$ java --sun-misc-unsafe-memory-access=warn -cp build Probe
+got Unsafe instance
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::allocateMemory has been called by Probe (file:/c25/)
+WARNING: Please consider reporting this to the maintainers of class Probe
+WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
+read back 42
+allocate/put/get/free all succeeded
+second use read 7
+
+$ java --sun-misc-unsafe-memory-access=debug -cp build Probe
+got Unsafe instance
+WARNING: sun.misc.Unsafe::allocateMemory called by Probe (file:/c25/)
+ at Probe.main(Probe.java:12)
+WARNING: sun.misc.Unsafe::putLong called by Probe (file:/c25/)
+ at Probe.main(Probe.java:13)
+WARNING: sun.misc.Unsafe::getLong called by Probe (file:/c25/)
+ at Probe.main(Probe.java:14)
+read back 42
+WARNING: sun.misc.Unsafe::freeMemory called by Probe (file:/c25/)
+ at Probe.main(Probe.java:15)
+allocate/put/get/free all succeeded
+WARNING: sun.misc.Unsafe::allocateMemory called by Probe (file:/c25/)
+ at Probe.main(Probe.java:21)
+WARNING: sun.misc.Unsafe::putLong called by Probe (file:/c25/)
+ at Probe.main(Probe.java:22)
+WARNING: sun.misc.Unsafe::getLong called by Probe (file:/c25/)
+ at Probe.main(Probe.java:23)
+second use read 7
+WARNING: sun.misc.Unsafe::freeMemory called by Probe (file:/c25/)
+ at Probe.main(Probe.java:24)
+
+$ java --sun-misc-unsafe-memory-access=deny -cp build Probe
+got Unsafe instance
+FAILED: java.lang.UnsupportedOperationException: allocateMemory
+second use FAILED: java.lang.UnsupportedOperationException: allocateMemory
+
+# the JVM also publishes the flag as a system property that libraries can read
+$ java tools/Prop.java (default)
+sun.misc.unsafe.memory.access=null
+$ java --sun-misc-unsafe-memory-access=allow tools/Prop.java
+sun.misc.unsafe.memory.access=allow
+$ java --sun-misc-unsafe-memory-access=warn tools/Prop.java
+sun.misc.unsafe.memory.access=warn
+$ java --sun-misc-unsafe-memory-access=debug tools/Prop.java
+sun.misc.unsafe.memory.access=debug
+$ java --sun-misc-unsafe-memory-access=deny tools/Prop.java
+sun.misc.unsafe.memory.access=deny
diff --git a/unsafe-migration/output/04-same-class-other-jdks.txt b/unsafe-migration/output/04-same-class-other-jdks.txt
new file mode 100644
index 0000000..62cd494
--- /dev/null
+++ b/unsafe-migration/output/04-same-class-other-jdks.txt
@@ -0,0 +1,25 @@
+$ java -cp build Probe (openjdk version "21.0.12.1" 2026-08-18, class file compiled by JDK 21)
+got Unsafe instance
+read back 42
+allocate/put/get/free all succeeded
+second use read 7
+
+$ java --sun-misc-unsafe-memory-access=deny -cp build Probe (openjdk version "21.0.12.1" 2026-08-18)
+Unrecognized option: --sun-misc-unsafe-memory-access=deny
+Error: Could not create the Java Virtual Machine.
+Error: A fatal exception has occurred. Program will exit.
+
+$ java -cp build Probe (openjdk version "26.0.2.1" 2026-08-18)
+got Unsafe instance
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::allocateMemory has been called by Probe (file:/c25/)
+WARNING: Please consider reporting this to the maintainers of class Probe
+...
+
+$ java -cp build Probe (openjdk version "27.0.1.0.2" 2026-10-21)
+got Unsafe instance
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::allocateMemory has been called by Probe (file:/c25/)
+WARNING: Please consider reporting this to the maintainers of class Probe
+...
+
diff --git a/unsafe-migration/output/05-jdeps-own-classes.txt b/unsafe-migration/output/05-jdeps-own-classes.txt
new file mode 100644
index 0000000..117c6c7
--- /dev/null
+++ b/unsafe-migration/output/05-jdeps-own-classes.txt
@@ -0,0 +1,14 @@
+$ jdeps --jdk-internals build (openjdk version "25.0.5" 2026-10-20 LTS)
+c25 -> jdk.unsupported
+ Probe -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
+ UnsafeAccess -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
+
+Warning: JDK internal APIs are unsupported and private to JDK implementation that are
+subject to be removed or changed incompatibly and could break your application.
+Please modify your code to eliminate dependence on any JDK internal APIs.
+For the most recent update on JDK internal API replacements, please check:
+https://wiki.openjdk.org/display/JDK8/Java+Dependency+Analysis+Tool
+
+JDK Internal API Suggested Replacement
+---------------- ---------------------
+sun.misc.Unsafe See https://openjdk.org/jeps/260
diff --git a/unsafe-migration/output/06-scan-libraries.txt b/unsafe-migration/output/06-scan-libraries.txt
new file mode 100644
index 0000000..ac3d7cf
--- /dev/null
+++ b/unsafe-migration/output/06-scan-libraries.txt
@@ -0,0 +1,109 @@
+sha1 ok io.netty:netty-common:4.2.20.Final
+sha1 ok io.netty:netty-common:4.1.140.Final
+sha1 ok io.netty:netty-common:4.1.100.Final
+sha1 ok com.google.guava:guava:33.8.0-jre
+sha1 ok com.google.guava:guava:32.1.3-jre
+sha1 ok org.apache.lucene:lucene-core:10.6.0
+sha1 ok org.apache.lucene:lucene-core:9.12.3
+sha1 ok com.esotericsoftware:kryo:5.7.1
+sha1 ok org.apache.hadoop:hadoop-common:3.5.0
+sha1 ok org.apache.hadoop:hadoop-common:3.3.6
+sha1 ok org.apache.cassandra:cassandra-all:5.0.9
+sha1 ok org.agrona:agrona:2.7.1
+sha1 ok org.jctools:jctools-core:4.0.7
+sha1 ok com.github.ben-manes.caffeine:caffeine:3.3.1
+sha1 ok com.google.protobuf:protobuf-java:4.36.2
+sha1 ok org.xerial.snappy:snappy-java:1.1.10.12
+sha1 ok org.apache.spark:spark-unsafe_2.13:4.3.0
+
+this JDK: sun.misc.Unsafe has 86 public methods, 83 deprecated for removal
+
+netty-common-4.2.20.Final.jar: 657 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 22
+ classes calling a removal-deprecated Unsafe method: 21 (125 call sites, 34 distinct methods)
+ methods: {addressSize=1, allocateMemory=4, arrayBaseOffset=5, arrayIndexScale=5, compareAndSwapInt=1, compareAndSwapLong=6, compareAndSwapObject=1, copyMemory=4, freeMemory=4, getAndAddInt=1, getBoolean=1, getByte=10, getChar=5, getInt=10, getIntVolatile=1, getLong=13, getLongVolatile=4, getObject=2, getObjectVolatile=2, getShort=3, objectFieldOffset=3, putByte=3, putInt=3, putLong=4, putObject=4, putOrderedInt=1, putOrderedLong=8, putOrderedObject=3, putShort=3, reallocateMemory=2, setMemory=2, staticFieldBase=2, staticFieldOffset=2, storeFence=2}
+
+netty-common-4.1.140.Final.jar: 548 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 22
+ classes calling a removal-deprecated Unsafe method: 21 (122 call sites, 33 distinct methods)
+ methods: {addressSize=1, allocateMemory=4, arrayBaseOffset=5, arrayIndexScale=5, compareAndSwapLong=6, compareAndSwapObject=1, copyMemory=4, freeMemory=4, getBoolean=1, getByte=10, getChar=5, getInt=10, getIntVolatile=1, getLong=16, getLongVolatile=1, getObject=2, getObjectVolatile=2, getShort=3, objectFieldOffset=3, putByte=3, putInt=3, putIntVolatile=1, putLong=4, putObject=4, putOrderedInt=1, putOrderedLong=8, putOrderedObject=3, putShort=2, reallocateMemory=2, setMemory=2, staticFieldBase=2, staticFieldOffset=2, storeFence=1}
+ other (not deprecated for removal) Unsafe methods called: [throwException]
+
+netty-common-4.1.100.Final.jar: 492 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 15
+ classes calling a removal-deprecated Unsafe method: 14 (107 call sites, 32 distinct methods)
+ methods: {addressSize=1, allocateMemory=3, arrayBaseOffset=4, arrayIndexScale=4, compareAndSwapLong=3, compareAndSwapObject=1, copyMemory=4, freeMemory=3, getBoolean=1, getByte=10, getChar=5, getInt=10, getIntVolatile=1, getLong=14, getObject=2, getObjectVolatile=2, getShort=3, objectFieldOffset=3, putByte=3, putInt=3, putIntVolatile=1, putLong=2, putObject=4, putOrderedInt=1, putOrderedLong=5, putOrderedObject=3, putShort=2, reallocateMemory=2, setMemory=2, staticFieldBase=2, staticFieldOffset=2, storeFence=1}
+ other (not deprecated for removal) Unsafe methods called: [throwException]
+
+guava-33.8.0-jre.jar: 1972 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 5
+ classes calling a removal-deprecated Unsafe method: 5 (22 call sites, 8 distinct methods)
+ methods: {arrayBaseOffset=2, arrayIndexScale=2, compareAndSwapObject=3, getAndSetObject=2, getLong=4, objectFieldOffset=5, putLong=2, putObject=2}
+ other (not deprecated for removal) Unsafe methods called: [getUnsafe]
+
+guava-32.1.3-jre.jar: 2020 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 9
+ classes calling a removal-deprecated Unsafe method: 9 (34 call sites, 10 distinct methods)
+ methods: {arrayBaseOffset=2, arrayIndexScale=2, compareAndSwapInt=2, compareAndSwapLong=4, compareAndSwapObject=3, getAndSetObject=2, getLong=4, objectFieldOffset=11, putLong=2, putObject=2}
+ other (not deprecated for removal) Unsafe methods called: [getUnsafe]
+
+lucene-core-10.6.0.jar: 2758 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 0
+ classes calling a removal-deprecated Unsafe method: 0 (0 call sites, 0 distinct methods)
+
+lucene-core-9.12.3.jar: 2515 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 1
+ classes calling a removal-deprecated Unsafe method: 0 (0 call sites, 0 distinct methods)
+
+kryo-5.7.1.jar: 246 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 15
+ classes calling a removal-deprecated Unsafe method: 15 (100 call sites, 20 distinct methods)
+ methods: {copyMemory=4, getBoolean=2, getByte=10, getChar=4, getDouble=4, getFloat=4, getInt=5, getLong=5, getObject=5, getShort=4, objectFieldOffset=11, putBoolean=2, putByte=10, putChar=4, putDouble=4, putFloat=4, putInt=5, putLong=5, putObject=4, putShort=4}
+
+hadoop-common-3.5.0.jar: 2633 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 4
+ classes calling a removal-deprecated Unsafe method: 1 (4 call sites, 3 distinct methods)
+ methods: {arrayBaseOffset=1, arrayIndexScale=1, getLong=2}
+ other (not deprecated for removal) Unsafe methods called: [pageSize]
+
+hadoop-common-3.3.6.jar: 2568 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 4
+ classes calling a removal-deprecated Unsafe method: 1 (4 call sites, 3 distinct methods)
+ methods: {arrayBaseOffset=1, arrayIndexScale=1, getLong=2}
+ other (not deprecated for removal) Unsafe methods called: [pageSize]
+
+cassandra-all-5.0.9.jar: 5770 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 7
+ classes calling a removal-deprecated Unsafe method: 6 (152 call sites, 16 distinct methods)
+ methods: {arrayBaseOffset=3, arrayIndexScale=1, copyMemory=7, getByte=46, getInt=6, getLong=15, getObject=2, getObjectVolatile=1, getShort=3, objectFieldOffset=7, putByte=43, putInt=9, putLong=4, putObject=1, putShort=2, setMemory=2}
+ other (not deprecated for removal) Unsafe methods called: [allocateInstance, getUnsafe, pageSize]
+
+agrona-2.7.1.jar: 295 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 0
+ classes calling a removal-deprecated Unsafe method: 0 (0 call sites, 0 distinct methods)
+
+jctools-core-4.0.7.jar: 356 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 58
+ classes calling a removal-deprecated Unsafe method: 57 (136 call sites, 17 distinct methods)
+ methods: {arrayBaseOffset=9, arrayIndexScale=8, compareAndSwapLong=22, compareAndSwapObject=14, getAndAddLong=4, getAndSetLong=2, getAndSetObject=2, getInt=2, getLong=2, getLongVolatile=20, getObject=2, getObjectVolatile=3, objectFieldOffset=2, putLong=3, putObject=5, putOrderedLong=28, putOrderedObject=8}
+ other (not deprecated for removal) Unsafe methods called: [pageSize]
+
+caffeine-3.3.1.jar: 801 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 0
+ classes calling a removal-deprecated Unsafe method: 0 (0 call sites, 0 distinct methods)
+
+protobuf-java-4.36.2.jar: 731 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 4
+ classes calling a removal-deprecated Unsafe method: 4 (210 call sites, 20 distinct methods)
+ methods: {arrayBaseOffset=2, arrayIndexScale=1, copyMemory=2, getBoolean=1, getByte=2, getDouble=1, getFloat=1, getInt=16, getLong=10, getObject=95, objectFieldOffset=5, putBoolean=1, putByte=2, putDouble=1, putFloat=1, putInt=34, putLong=5, putObject=28, staticFieldBase=1, staticFieldOffset=1}
+ other (not deprecated for removal) Unsafe methods called: [allocateInstance]
+
+snappy-java-1.1.10.12.jar: 40 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 1
+ classes calling a removal-deprecated Unsafe method: 0 (0 call sites, 0 distinct methods)
+
+spark-unsafe_2.13-4.3.0.jar: 66 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 1
+ classes calling a removal-deprecated Unsafe method: 1 (35 call sites, 24 distinct methods)
+ methods: {allocateMemory=2, arrayBaseOffset=7, copyMemory=2, freeMemory=1, getBoolean=2, getByte=1, getDouble=1, getFloat=1, getInt=1, getLong=2, getObjectVolatile=1, getShort=1, objectFieldOffset=1, putBoolean=1, putByte=1, putDouble=1, putFloat=1, putInt=1, putLong=1, putObjectVolatile=1, putShort=1, setMemory=2, staticFieldBase=1, staticFieldOffset=1}
+ other (not deprecated for removal) Unsafe methods called: [throwException]
diff --git a/unsafe-migration/output/07-jdeps-netty.txt b/unsafe-migration/output/07-jdeps-netty.txt
new file mode 100644
index 0000000..a48fb27
--- /dev/null
+++ b/unsafe-migration/output/07-jdeps-netty.txt
@@ -0,0 +1,11 @@
+$ jdeps --jdk-internals netty-common-4.1.140.Final.jar (excerpt)
+netty-common-4.1.140.Final.jar -> jdk.unsupported
+ io.netty.util.internal.CleanerJava9 -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
+ io.netty.util.internal.CleanerJava9$1 -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
+ io.netty.util.internal.CleanerJava9$2 -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
+JDK Internal API Suggested Replacement
+---------------- ---------------------
+sun.misc.Unsafe See https://openjdk.org/jeps/260
+
+$ jdeps --jdk-internals netty-common-4.1.140.Final.jar | grep -c -- '-> sun.misc.Unsafe'
+27
diff --git a/unsafe-migration/output/08-netty-runtime.txt b/unsafe-migration/output/08-netty-runtime.txt
new file mode 100644
index 0000000..6eed06a
--- /dev/null
+++ b/unsafe-migration/output/08-netty-runtime.txt
@@ -0,0 +1,48 @@
+$ java -cp netty-4.1.140.Final NettyProbe (JDK 25 default)
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/netty-common-4.1.140.Final.jar)
+WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
+WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
+netty sees Unsafe: true
+direct=true hasMemoryAddress=true readLong=cafebabe
+done
+
+$ java --sun-misc-unsafe-memory-access=allow -cp netty-4.1.140.Final NettyProbe
+netty sees Unsafe: true
+direct=true hasMemoryAddress=true readLong=cafebabe
+done
+
+$ java --sun-misc-unsafe-memory-access=deny -cp netty-4.1.140.Final NettyProbe
+netty sees Unsafe: false
+direct=true hasMemoryAddress=false readLong=cafebabe
+done
+
+$ java --sun-misc-unsafe-memory-access=debug -cp netty-4.1.140.Final NettyProbe
+netty sees Unsafe: false
+direct=true hasMemoryAddress=false readLong=cafebabe
+done
+
+$ java -cp netty-4.2.20.Final NettyProbe (JDK 25 default)
+netty sees Unsafe: false
+direct=true hasMemoryAddress=true readLong=cafebabe
+done
+
+$ java --sun-misc-unsafe-memory-access=allow -cp netty-4.2.20.Final NettyProbe
+netty sees Unsafe: true
+direct=true hasMemoryAddress=true readLong=cafebabe
+done
+
+$ java --sun-misc-unsafe-memory-access=deny -cp netty-4.2.20.Final NettyProbe
+netty sees Unsafe: false
+direct=true hasMemoryAddress=true readLong=cafebabe
+done
+
+$ java --sun-misc-unsafe-memory-access=debug -cp netty-4.2.20.Final NettyProbe
+netty sees Unsafe: false
+direct=true hasMemoryAddress=true readLong=cafebabe
+done
+
+$ unzip -p netty-common-4.1.140.Final.jar io/netty/util/internal/PlatformDependent0.class | grep -a -o -F 'sun.misc.unsafe.memory.access'
+sun.misc.unsafe.memory.access
+$ unzip -p netty-common-4.2.20.Final.jar io/netty/util/internal/PlatformDependent0.class | grep -a -o -F 'sun.misc.unsafe.memory.access'
+sun.misc.unsafe.memory.access
diff --git a/unsafe-migration/output/09-equivalence.txt b/unsafe-migration/output/09-equivalence.txt
new file mode 100644
index 0000000..6537178
--- /dev/null
+++ b/unsafe-migration/output/09-equivalence.txt
@@ -0,0 +1,42 @@
+$ java --sun-misc-unsafe-memory-access=allow -cp build Equivalence (Unsafe, VarHandle and FFM side by side)
+field CAS: Unsafe and the replacement gave the same 2000-step trace
+field getAndAdd: 4 threads x 100000 -> total 400000 on every implementation
+off-heap array: Unsafe and the replacement gave the same 2000-step trace
+off-heap getAndAdd: 4 threads x 100000 -> sum 400000 on every implementation
+copyFrom / clear: identical on every implementation
+byte[] long access at offsets 0..32: Unsafe and VarHandle view agree
+VarHandle view at offset 36 of a 40-byte array: ArrayIndexOutOfBoundsException
+FFM read of element 4 in a 4-element segment: IndexOutOfBoundsException
+FFM read after close: IllegalStateException: Already closed
+ALL 170 CHECKS PASSED
+exit=0
+
+$ java --sun-misc-unsafe-memory-access=deny -cp build Equivalence --new-only (replacements only)
+field CAS: the replacement alone gave the same 2000-step trace
+field getAndAdd: 4 threads x 100000 -> total 400000 on every implementation
+off-heap array: the replacement alone gave the same 2000-step trace
+off-heap getAndAdd: 4 threads x 100000 -> sum 400000 on every implementation
+copyFrom / clear: identical on every implementation
+byte[] long access at offsets 0..32: VarHandle view only
+VarHandle view at offset 36 of a 40-byte array: ArrayIndexOutOfBoundsException
+FFM read of element 4 in a 4-element segment: IndexOutOfBoundsException
+FFM read after close: IllegalStateException: Already closed
+ALL 67 CHECKS PASSED (new APIs only)
+exit=0
+
+$ java --sun-misc-unsafe-memory-access=deny -cp build Equivalence (the Unsafe version under deny)
+Exception in thread "main" java.lang.ExceptionInInitializerError
+ at Equivalence.main(Equivalence.java:25)
+Caused by: java.lang.UnsupportedOperationException: objectFieldOffset
+ at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccessSlow(Unsafe.java:1822)
+ at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccess(Unsafe.java:1787)
+ at jdk.unsupported/sun.misc.Unsafe.objectFieldOffset(Unsafe.java:905)
+
+$ java -cp build Equivalence (JDK 25 default: one warning, then the same results)
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::objectFieldOffset has been called by UnsafeFieldCounter (file:/m/)
+WARNING: Please consider reporting this to the maintainers of class UnsafeFieldCounter
+WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
+field CAS: Unsafe and the replacement gave the same 2000-step trace
+field getAndAdd: 4 threads x 100000 -> total 400000 on every implementation
+off-heap array: Unsafe and the replacement gave the same 2000-step trace
diff --git a/unsafe-migration/output/10-jfr-deprecated-invocation.txt b/unsafe-migration/output/10-jfr-deprecated-invocation.txt
new file mode 100644
index 0000000..fe8bc1b
--- /dev/null
+++ b/unsafe-migration/output/10-jfr-deprecated-invocation.txt
@@ -0,0 +1,14 @@
+$ java -XX:StartFlightRecording:filename=probe.jfr --sun-misc-unsafe-memory-access=allow -cp build Probe (silent: no warnings)
+got Unsafe instance
+read back 42
+allocate/put/get/free all succeeded
+second use read 7
+$ jfr print --events jdk.DeprecatedInvocation probe.jfr | grep -E 'method = |line:' | sort | uniq -c
+ 1 Probe.main(String[]) line: 12
+ 1 Probe.main(String[]) line: 13
+ 1 Probe.main(String[]) line: 14
+ 1 Probe.main(String[]) line: 15
+ 1 method = sun.misc.Unsafe.allocateMemory(long)
+ 1 method = sun.misc.Unsafe.freeMemory(long)
+ 1 method = sun.misc.Unsafe.getLong(long)
+ 1 method = sun.misc.Unsafe.putLong(long, long)
diff --git a/unsafe-migration/output/11-scan-callers.txt b/unsafe-migration/output/11-scan-callers.txt
new file mode 100644
index 0000000..b3bcd59
--- /dev/null
+++ b/unsafe-migration/output/11-scan-callers.txt
@@ -0,0 +1,75 @@
+sha1 ok com.google.guava:guava:33.8.0-jre
+sha1 ok com.esotericsoftware:kryo:5.7.1
+sha1 ok org.apache.hadoop:hadoop-common:3.5.0
+sha1 ok org.apache.cassandra:cassandra-all:5.0.9
+sha1 ok com.google.protobuf:protobuf-java:4.36.2
+sha1 ok org.apache.spark:spark-unsafe_2.13:4.3.0
+
+this JDK: sun.misc.Unsafe has 86 public methods, 83 deprecated for removal
+
+guava-33.8.0-jre.jar: 1972 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 5
+ classes calling a removal-deprecated Unsafe method: 5 (22 call sites, 8 distinct methods)
+ methods: {arrayBaseOffset=2, arrayIndexScale=2, compareAndSwapObject=3, getAndSetObject=2, getLong=4, objectFieldOffset=5, putLong=2, putObject=2}
+ other (not deprecated for removal) Unsafe methods called: [getUnsafe]
+ com.google.common.hash.LittleEndianByteArray$UnsafeByteArray 2
+ com.google.common.hash.LittleEndianByteArray$UnsafeByteArray$1 2
+ com.google.common.hash.LittleEndianByteArray$UnsafeByteArray$2 2
+ com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator 4
+ com.google.common.util.concurrent.AbstractFutureState$UnsafeAtomicHelper 12
+
+kryo-5.7.1.jar: 246 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 15
+ classes calling a removal-deprecated Unsafe method: 15 (100 call sites, 20 distinct methods)
+ methods: {copyMemory=4, getBoolean=2, getByte=10, getChar=4, getDouble=4, getFloat=4, getInt=5, getLong=5, getObject=5, getShort=4, objectFieldOffset=11, putBoolean=2, putByte=10, putChar=4, putDouble=4, putFloat=4, putInt=5, putLong=5, putObject=4, putShort=4}
+ com.esotericsoftware.kryo.serializers.UnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$BooleanUnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$ByteUnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$CharUnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$DoubleUnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$FloatUnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$IntUnsafeField 7
+ com.esotericsoftware.kryo.serializers.UnsafeField$LongUnsafeField 7
+ com.esotericsoftware.kryo.serializers.UnsafeField$ShortUnsafeField 5
+ com.esotericsoftware.kryo.serializers.UnsafeField$StringUnsafeField 5
+ com.esotericsoftware.kryo.unsafe.UnsafeByteBufferInput 11
+ com.esotericsoftware.kryo.unsafe.UnsafeByteBufferOutput 11
+ com.esotericsoftware.kryo.unsafe.UnsafeInput 11
+ com.esotericsoftware.kryo.unsafe.UnsafeOutput 11
+ com.esotericsoftware.kryo.unsafe.UnsafeUtil 2
+
+hadoop-common-3.5.0.jar: 2633 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 4
+ classes calling a removal-deprecated Unsafe method: 1 (4 call sites, 3 distinct methods)
+ methods: {arrayBaseOffset=1, arrayIndexScale=1, getLong=2}
+ other (not deprecated for removal) Unsafe methods called: [pageSize]
+ org.apache.hadoop.io.FastByteComparisons$LexicographicalComparerHolder$UnsafeComparer 4
+
+cassandra-all-5.0.9.jar: 5770 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 7
+ classes calling a removal-deprecated Unsafe method: 6 (152 call sites, 16 distinct methods)
+ methods: {arrayBaseOffset=3, arrayIndexScale=1, copyMemory=7, getByte=46, getInt=6, getLong=15, getObject=2, getObjectVolatile=1, getShort=3, objectFieldOffset=7, putByte=43, putInt=9, putLong=4, putObject=1, putShort=2, setMemory=2}
+ other (not deprecated for removal) Unsafe methods called: [allocateInstance, getUnsafe, pageSize]
+ org.apache.cassandra.io.util.Memory 5
+ org.apache.cassandra.utils.FastByteOperations$UnsafeOperations 16
+ org.apache.cassandra.utils.concurrent.Ref 3
+ org.apache.cassandra.utils.memory.LittleEndianMemoryUtil 37
+ org.apache.cassandra.utils.memory.MemoryUtil 29
+ org.apache.cassandra.utils.memory.NativeEndianMemoryUtil 62
+
+protobuf-java-4.36.2.jar: 731 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 4
+ classes calling a removal-deprecated Unsafe method: 4 (210 call sites, 20 distinct methods)
+ methods: {arrayBaseOffset=2, arrayIndexScale=1, copyMemory=2, getBoolean=1, getByte=2, getDouble=1, getFloat=1, getInt=16, getLong=10, getObject=95, objectFieldOffset=5, putBoolean=1, putByte=2, putDouble=1, putFloat=1, putInt=34, putLong=5, putObject=28, staticFieldBase=1, staticFieldOffset=1}
+ other (not deprecated for removal) Unsafe methods called: [allocateInstance]
+ com.google.protobuf.MessageSchema 182
+ com.google.protobuf.UnsafeUtil 1
+ com.google.protobuf.UnsafeUtil$JvmMemoryAccessor 18
+ com.google.protobuf.UnsafeUtil$MemoryAccessor 9
+
+spark-unsafe_2.13-4.3.0.jar: 66 classes scanned
+ classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): 1
+ classes calling a removal-deprecated Unsafe method: 1 (35 call sites, 24 distinct methods)
+ methods: {allocateMemory=2, arrayBaseOffset=7, copyMemory=2, freeMemory=1, getBoolean=2, getByte=1, getDouble=1, getFloat=1, getInt=1, getLong=2, getObjectVolatile=1, getShort=1, objectFieldOffset=1, putBoolean=1, putByte=1, putDouble=1, putFloat=1, putInt=1, putLong=1, putObjectVolatile=1, putShort=1, setMemory=2, staticFieldBase=1, staticFieldOffset=1}
+ other (not deprecated for removal) Unsafe methods called: [throwException]
+ org.apache.spark.unsafe.Platform 35
diff --git a/unsafe-migration/output/12-guava-runtime.txt b/unsafe-migration/output/12-guava-runtime.txt
new file mode 100644
index 0000000..0390312
--- /dev/null
+++ b/unsafe-migration/output/12-guava-runtime.txt
@@ -0,0 +1,45 @@
+$ java -cp guava-32.1.3-jre GuavaProbe (JDK 25 default)
+murmur3_128 = 1df72d8cb9e9c0755a1e5df367505d00
+WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
+WARNING: sun.misc.Unsafe::arrayBaseOffset has been called by com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator (file:/guava-32.1.3-jre.jar)
+WARNING: Please consider reporting this to the maintainers of class com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator
+WARNING: sun.misc.Unsafe::arrayBaseOffset will be removed in a future release
+comparator = com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator
+compare = -1
+future = ok
+
+$ java --sun-misc-unsafe-memory-access=debug -cp guava-32.1.3-jre GuavaProbe
+murmur3_128 = 1df72d8cb9e9c0755a1e5df367505d00
+WARNING: sun.misc.Unsafe::arrayBaseOffset called by com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator (file:/guava-32.1.3-jre.jar)
+ at com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator.(UnsignedBytes.java:317)
+ at java.base/java.lang.Class.forName0(Native Method)
+ at java.base/java.lang.Class.forName(Class.java:467)
+ at java.base/java.lang.Class.forName(Class.java:458)
+ at com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder.getBestComparator(UnsignedBytes.java:437)
+ at com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder.(UnsignedBytes.java:290)
+ at com.google.common.primitives.UnsignedBytes.lexicographicalComparator(UnsignedBytes.java:270)
+
+$ java --sun-misc-unsafe-memory-access=deny -cp guava-32.1.3-jre GuavaProbe
+murmur3_128 = 1df72d8cb9e9c0755a1e5df367505d00
+comparator = com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$PureJavaComparator
+compare = -1
+future = ok
+
+$ java -cp guava-33.8.0-jre GuavaProbe (JDK 25 default)
+murmur3_128 = 1df72d8cb9e9c0755a1e5df367505d00
+comparator = com.google.common.primitives.UnsignedBytes$ArraysCompareUnsignedComparator
+compare = -1
+future = ok
+
+$ java --sun-misc-unsafe-memory-access=debug -cp guava-33.8.0-jre GuavaProbe
+murmur3_128 = 1df72d8cb9e9c0755a1e5df367505d00
+comparator = com.google.common.primitives.UnsignedBytes$ArraysCompareUnsignedComparator
+compare = -1
+future = ok
+
+$ java --sun-misc-unsafe-memory-access=deny -cp guava-33.8.0-jre GuavaProbe
+murmur3_128 = 1df72d8cb9e9c0755a1e5df367505d00
+comparator = com.google.common.primitives.UnsignedBytes$ArraysCompareUnsignedComparator
+compare = -1
+future = ok
+
diff --git a/unsafe-migration/run.sh b/unsafe-migration/run.sh
new file mode 100644
index 0000000..1a32d3e
--- /dev/null
+++ b/unsafe-migration/run.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+# unsafe-migration: regenerates output/*.txt. JDK25=/path JDK21=/path [JDK26=/path JDK27=/path] ./run.sh
+exec "$(cd "$(dirname "$0")" && pwd)/scripts/run-all.sh" "$@"
diff --git a/unsafe-migration/scripts/run-all.sh b/unsafe-migration/scripts/run-all.sh
new file mode 100644
index 0000000..c124dd5
--- /dev/null
+++ b/unsafe-migration/scripts/run-all.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+# Regenerates every file in output/.
+# JDK25=/path JDK21=/path [JDK26=/path JDK27=/path] ./scripts/run-all.sh
+# Steps 06 and 07 download jars from Maven Central (sha1-checked); everything else is offline.
+set -uo pipefail
+unset JAVA_TOOL_OPTIONS
+HERE="$(cd "$(dirname "$0")/.." && pwd)"
+JDK25="${JDK25:-/tmp/tools/jdk-25.0.5+8}"
+JDK21="${JDK21:-/usr/lib/jvm/java-21-openjdk-amd64}"
+JDK26="${JDK26:-}"; JDK27="${JDK27:-}"
+OUT="$HERE/output"; mkdir -p "$OUT"
+B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT
+cd "$HERE"
+F='--sun-misc-unsafe-memory-access'
+norm() { sed "s#$B##g"; } # keep temp-dir paths out of the transcripts
+ver() { "$1/bin/java" -version 2>&1 | sed -n 1p; }
+
+# ---- 01 which JDK did what ----------------------------------------------------------------
+{ for j in "$JDK21" "$JDK25" "$JDK26" "$JDK27"; do [ -n "$j" ] && { echo "== $(ver "$j")"; "$j/bin/java" tools/ListUnsafe.java 2>&1; echo; }; done
+} > "$OUT/01-unsafe-methods-per-jdk.txt"
+
+# ---- 02 compile-time warnings -------------------------------------------------------------
+mkdir -p "$B/c25" "$B/c21"
+{ echo "\$ javac src/UnsafeAccess.java broken/Probe.java ($(ver "$JDK25"))"
+ "$JDK25/bin/javac" -d "$B/c25" src/UnsafeAccess.java broken/Probe.java 2>&1 | grep -E 'warning|^[0-9]+ warnings?'
+ echo; echo "\$ javac src/UnsafeAccess.java broken/Probe.java ($(ver "$JDK21"))"
+ "$JDK21/bin/javac" -d "$B/c21" src/UnsafeAccess.java broken/Probe.java 2>&1 | grep -E 'warning|^[0-9]+ warnings?'
+ echo; echo "\$ javac -Xlint:removal -Werror src/UnsafeAccess.java broken/Probe.java ($(ver "$JDK25"))"
+ "$JDK25/bin/javac" -Xlint:removal -Werror -d "$B/w" src/UnsafeAccess.java broken/Probe.java 2>&1 | grep -E 'removal|error|^[0-9]+ (error|warning)' | head -6; echo "exit=${PIPESTATUS[0]}"
+} > "$OUT/02-compile-warnings.txt"
+
+# ---- 03 the four flag values on JDK 25 ----------------------------------------------------
+{ for a in "" allow warn debug deny; do
+ [ -z "$a" ] && { echo "\$ java -cp build Probe (JDK 25 default)"; "$JDK25/bin/java" -cp "$B/c25" Probe 2>&1 | norm; } \
+ || { echo "\$ java $F=$a -cp build Probe"; "$JDK25/bin/java" "$F=$a" -cp "$B/c25" Probe 2>&1 | norm; }
+ echo
+ done
+ echo "# the JVM also publishes the flag as a system property that libraries can read"
+ for a in "" allow warn debug deny; do
+ [ -z "$a" ] && { echo "\$ java tools/Prop.java (default)"; "$JDK25/bin/java" tools/Prop.java 2>&1; } \
+ || { echo "\$ java $F=$a tools/Prop.java"; "$JDK25/bin/java" "$F=$a" tools/Prop.java 2>&1; }
+ done
+} > "$OUT/03-flag-values-jdk25.txt"
+
+# ---- 04 same class, other JDKs ------------------------------------------------------------
+{ echo "\$ java -cp build Probe ($(ver "$JDK21"), class file compiled by JDK 21)"; "$JDK21/bin/java" -cp "$B/c21" Probe 2>&1 | norm; echo
+ echo "\$ java $F=deny -cp build Probe ($(ver "$JDK21"))"; "$JDK21/bin/java" "$F=deny" -cp "$B/c21" Probe 2>&1 | norm; echo
+ for j in "$JDK26" "$JDK27"; do [ -n "$j" ] && { echo "\$ java -cp build Probe ($(ver "$j"))"; "$j/bin/java" -cp "$B/c25" Probe 2>&1 | norm | head -4; echo "..."; echo; }; done
+} > "$OUT/04-same-class-other-jdks.txt"
+
+# ---- 05 jdeps on our own classes ----------------------------------------------------------
+{ echo "\$ jdeps --jdk-internals build ($(ver "$JDK25"))"; "$JDK25/bin/jdeps" --jdk-internals "$B/c25" 2>&1 | norm
+} > "$OUT/05-jdeps-own-classes.txt"
+
+# ---- 06/07 real libraries -----------------------------------------------------------------
+online=no; for i in 1 2 3 4 5; do curl -sf -o /dev/null https://repo1.maven.org/maven2/io/netty/netty-common/maven-metadata.xml && { online=yes; break; }; sleep 1; done
+if [ $online = yes ]; then
+ JDK25="$JDK25" "$HERE/scripts/scan-libs.sh" > "$OUT/06-scan-libraries.txt" 2>&1
+ ONLY='guava:33|hadoop-common:3.5|protobuf|cassandra|spark|kryo' JDK25="$JDK25" "$HERE/scripts/scan-libs.sh" --classes > "$OUT/11-scan-callers.txt" 2>&1
+ L="$B/lib"; mkdir -p "$L"
+ fetch() { for i in 1 2 3 4 5 6; do curl -sfL -o "$L/$(basename "$1")" "https://repo1.maven.org/maven2/$1" && return 0; sleep 1; done; return 1; }
+ for v in 4.1.140.Final 4.2.20.Final; do fetch io/netty/netty-common/$v/netty-common-$v.jar; fetch io/netty/netty-buffer/$v/netty-buffer-$v.jar; done
+ { echo "\$ jdeps --jdk-internals netty-common-4.1.140.Final.jar (excerpt)"
+ "$JDK25/bin/jdeps" --jdk-internals --multi-release 25 "$L/netty-common-4.1.140.Final.jar" 2>&1 | sed -n '1,4p;/^JDK Internal API/,$p'
+ echo; echo "\$ jdeps --jdk-internals netty-common-4.1.140.Final.jar | grep -c -- '-> sun.misc.Unsafe'"
+ "$JDK25/bin/jdeps" --jdk-internals --multi-release 25 "$L/netty-common-4.1.140.Final.jar" 2>&1 | grep -c -- '-> sun.misc.Unsafe'
+ } > "$OUT/07-jdeps-netty.txt"
+ {
+ for v in 4.1.140.Final 4.2.20.Final; do
+ CP="$L/netty-common-$v.jar:$L/netty-buffer-$v.jar"; mkdir -p "$B/n$v"; "$JDK25/bin/javac" -cp "$CP" -d "$B/n$v" tools/NettyProbe.java 2>&1
+ for a in "" allow deny debug; do
+ [ -z "$a" ] && echo "\$ java -cp netty-$v NettyProbe (JDK 25 default)" || echo "\$ java $F=$a -cp netty-$v NettyProbe"
+ { [ -z "$a" ] && "$JDK25/bin/java" -cp "$CP:$B/n$v" NettyProbe || "$JDK25/bin/java" "$F=$a" -cp "$CP:$B/n$v" NettyProbe; } 2>&1 | sed "s#$L##g" | head -9
+ echo
+ done
+ done
+ for v in 4.1.140.Final 4.2.20.Final; do
+ echo "\$ unzip -p netty-common-$v.jar io/netty/util/internal/PlatformDependent0.class | grep -a -o -F 'sun.misc.unsafe.memory.access'"
+ unzip -p "$L/netty-common-$v.jar" io/netty/util/internal/PlatformDependent0.class | grep -a -o -F 'sun.misc.unsafe.memory.access' | sort -u
+ done
+ } > "$OUT/08-netty-runtime.txt"
+ # Guava 32.1.3 vs 33.8.0 on JDK 25: same three calls, different behaviour
+ fetch com/google/guava/failureaccess/1.0.3/failureaccess-1.0.3.jar
+ for v in 32.1.3-jre 33.8.0-jre; do fetch com/google/guava/guava/$v/guava-$v.jar; done
+ mkdir -p "$B/gp"; "$JDK25/bin/javac" -cp "$L/guava-33.8.0-jre.jar:$L/failureaccess-1.0.3.jar" -d "$B/gp" tools/GuavaProbe.java >/dev/null 2>&1
+ { for v in 32.1.3-jre 33.8.0-jre; do
+ CP="$L/guava-$v.jar:$L/failureaccess-1.0.3.jar:$B/gp"
+ for a in "" debug deny; do
+ [ -z "$a" ] && echo "\$ java -cp guava-$v GuavaProbe (JDK 25 default)" || echo "\$ java $F=$a -cp guava-$v GuavaProbe"
+ { [ -z "$a" ] && "$JDK25/bin/java" -cp "$CP" GuavaProbe || "$JDK25/bin/java" "$F=$a" -cp "$CP" GuavaProbe; } 2>&1 | sed "s#$L##g" | head -9
+ echo
+ done
+ done
+ } > "$OUT/12-guava-runtime.txt"
+else
+ echo "no network: skipped 06-08"
+fi
+
+# ---- 10 JFR: finds the callers even with =allow ------------------------------------------
+jfrsum() { "$JDK25/bin/jfr" print --events jdk.DeprecatedInvocation "$1" | grep -E 'method = |line:' | sed 's/^ *//' | sort | uniq -c | sed "s#^ *# #"; }
+{ echo "\$ java -XX:StartFlightRecording:filename=probe.jfr $F=allow -cp build Probe (silent: no warnings)"
+ "$JDK25/bin/java" "-XX:StartFlightRecording:filename=$B/probe.jfr" "$F=allow" -cp "$B/c25" Probe 2>&1 | grep -v 'jfr,startup'
+ echo "\$ jfr print --events jdk.DeprecatedInvocation probe.jfr | grep -E 'method = |line:' | sort | uniq -c"; jfrsum "$B/probe.jfr"
+} > "$OUT/10-jfr-deprecated-invocation.txt"
+
+# ---- 09 the migration: before/after equivalence -------------------------------------------
+mkdir -p "$B/m"; "$JDK25/bin/javac" -nowarn -d "$B/m" src/*.java 2>&1 | grep -E 'error'
+{ echo "\$ java $F=allow -cp build Equivalence (Unsafe, VarHandle and FFM side by side)"; "$JDK25/bin/java" "$F=allow" -cp "$B/m" Equivalence 2>&1; echo "exit=$?"; echo
+ echo "\$ java $F=deny -cp build Equivalence --new-only (replacements only)"; "$JDK25/bin/java" "$F=deny" -cp "$B/m" Equivalence --new-only 2>&1; echo "exit=$?"; echo
+ echo "\$ java $F=deny -cp build Equivalence (the Unsafe version under deny)"; "$JDK25/bin/java" "$F=deny" -cp "$B/m" Equivalence 2>&1 | head -6 | norm; echo
+ echo "\$ java -cp build Equivalence (JDK 25 default: one warning, then the same results)"; "$JDK25/bin/java" -cp "$B/m" Equivalence 2>&1 | norm | head -7
+} > "$OUT/09-equivalence.txt"
+echo "wrote $OUT"
diff --git a/unsafe-migration/scripts/scan-libs.sh b/unsafe-migration/scripts/scan-libs.sh
new file mode 100644
index 0000000..b77d59f
--- /dev/null
+++ b/unsafe-migration/scripts/scan-libs.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# Download real jars from Maven Central (sha1-checked) into a throw-away dir and scan them with tools/ScanJar.java.
+# JDK25=/path/to/jdk-25 ./scripts/scan-libs.sh [--classes] needs network; ONLY='regex' limits the coordinate list
+set -uo pipefail
+unset JAVA_TOOL_OPTIONS
+HERE="$(cd "$(dirname "$0")/.." && pwd)"
+JDK25="${JDK25:-/tmp/tools/jdk-25.0.5+8}"
+D="$(mktemp -d)"; trap 'rm -rf "$D"' EXIT
+COORDS="
+io.netty:netty-common:4.2.20.Final
+io.netty:netty-common:4.1.140.Final
+io.netty:netty-common:4.1.100.Final
+com.google.guava:guava:33.8.0-jre
+com.google.guava:guava:32.1.3-jre
+org.apache.lucene:lucene-core:10.6.0
+org.apache.lucene:lucene-core:9.12.3
+com.esotericsoftware:kryo:5.7.1
+org.apache.hadoop:hadoop-common:3.5.0
+org.apache.hadoop:hadoop-common:3.3.6
+org.apache.cassandra:cassandra-all:5.0.9
+org.agrona:agrona:2.7.1
+org.jctools:jctools-core:4.0.7
+com.github.ben-manes.caffeine:caffeine:3.3.1
+com.google.protobuf:protobuf-java:4.36.2
+org.xerial.snappy:snappy-java:1.1.10.12
+org.apache.spark:spark-unsafe_2.13:4.3.0
+"
+JARS=()
+for c in $(echo "$COORDS" | grep -E "${ONLY:-.}"); do
+ IFS=: read -r g a v <<<"$c"
+ url="https://repo1.maven.org/maven2/${g//.//}/$a/$v/$a-$v.jar"
+ ok=no
+ for try in 1 2 3 4 5 6 7 8; do sleep 1; # the sandbox proxy occasionally truncates a transfer; the sha1 check catches it
+ curl -sfL -o "$D/$a-$v.jar" "$url" || continue
+ want="$(curl -sfL "$url.sha1" | cut -c1-40)"; got="$(sha1sum "$D/$a-$v.jar" | cut -c1-40)"
+ [ -n "$want" ] && [ "$want" = "$got" ] && { ok=yes; break; }
+ done
+ [ $ok = yes ] && echo "sha1 ok $c" || { echo "FAILED (download or sha1) $c"; continue; }
+ JARS+=("$D/$a-$v.jar")
+done
+echo
+"$JDK25/bin/java" "$HERE/tools/ScanJar.java" "$@" "${JARS[@]}" 2>&1 | sed "s#$D/##g"
diff --git a/unsafe-migration/src/Equivalence.java b/unsafe-migration/src/Equivalence.java
new file mode 100644
index 0000000..0cd9c82
--- /dev/null
+++ b/unsafe-migration/src/Equivalence.java
@@ -0,0 +1,141 @@
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Supplier;
+
+/**
+ * Before/after equivalence check, no benchmarks: the same scripted operations run against the Unsafe version and the
+ * replacement, and every observable result must match. Prints one line per check; exits 1 on the first mismatch.
+ *
+ * java Equivalence all three implementations (needs Unsafe memory access allowed or warned)
+ * java Equivalence --new-only only VarHandle and FFM; run this one under --sun-misc-unsafe-memory-access=deny
+ */
+public class Equivalence {
+ static int checks;
+
+ public static void main(String[] args) throws Exception {
+ boolean newOnly = args.length > 0 && args[0].equals("--new-only");
+
+ // ---- 1. field CAS: scripted single-thread ops ----
+ List> fields = new ArrayList<>();
+ if (!newOnly) fields.add(UnsafeFieldCounter::new);
+ fields.add(VarHandleFieldCounter::new);
+ long[] ref = null;
+ for (var f : fields) {
+ long[] trace = fieldTrace(f.get());
+ if (ref == null) ref = trace; else same("field trace", ref, trace);
+ }
+ System.out.println("field CAS: " + names(fields.size()) + " gave the same 2000-step trace");
+
+ // ---- 2. field CAS under contention: lost updates would show as a wrong total ----
+ for (var f : fields) require("contended total", 400_000L, fieldTotal(f.get()));
+ System.out.println("field getAndAdd: 4 threads x 100000 -> total 400000 on every implementation");
+
+ // ---- 3. off-heap array ----
+ List> offs = new ArrayList<>();
+ if (!newOnly) offs.add(() -> new UnsafeOffHeapLongs(64));
+ offs.add(() -> new FfmOffHeapLongs(64));
+ long[] ref2 = null;
+ for (var s : offs) {
+ try (OffHeapLongs o = s.get()) {
+ long[] trace = offHeapTrace(o);
+ if (ref2 == null) ref2 = trace; else same("off-heap trace", ref2, trace);
+ }
+ }
+ System.out.println("off-heap array: " + names(offs.size()) + " gave the same 2000-step trace");
+
+ for (var s : offs) {
+ try (OffHeapLongs o = s.get()) {
+ Thread[] ts = new Thread[4];
+ for (int t = 0; t < ts.length; t++) { ts[t] = new Thread(() -> { for (int k = 0; k < 100_000; k++) o.getAndAdd(k % 4, 1); }); ts[t].start(); }
+ for (Thread t : ts) t.join();
+ long sum = 0; for (int i = 0; i < 4; i++) sum += o.get(i);
+ require("off-heap contended sum", 400_000L, sum);
+ }
+ }
+ System.out.println("off-heap getAndAdd: 4 threads x 100000 -> sum 400000 on every implementation");
+
+ // ---- 4. bulk copy ----
+ for (var s : offs) {
+ try (OffHeapLongs a = s.get(); OffHeapLongs b = s.get()) {
+ for (int i = 0; i < a.length(); i++) a.set(i, i * 31L);
+ b.copyFrom(a);
+ for (int i = 0; i < a.length(); i++) require("copy[" + i + "]", i * 31L, b.get(i));
+ a.clear();
+ require("cleared", 0L, a.get(5));
+ }
+ }
+ System.out.println("copyFrom / clear: identical on every implementation");
+
+ // ---- 5. byte[] views ----
+ byte[] data = new byte[40];
+ new Random(7).nextBytes(data);
+ for (int off = 0; off <= 32; off++) {
+ long viewed = ViewBytes.get(data, off);
+ if (!newOnly) require("byte[] getLong @" + off, UnsafeBytes.get(data, off), viewed);
+ }
+ byte[] x = new byte[16], y = new byte[16];
+ ViewBytes.put(x, 3, 0x0102030405060708L);
+ if (!newOnly) { UnsafeBytes.put(y, 3, 0x0102030405060708L); require("byte[] put", java.util.Arrays.toString(y), java.util.Arrays.toString(x)); }
+ System.out.println("byte[] long access at offsets 0..32: " + (newOnly ? "VarHandle view only" : "Unsafe and VarHandle view agree"));
+
+ // ---- 6. what the replacement adds: a bounds check instead of a read past the end ----
+ try { ViewBytes.get(data, 36); System.out.println("view past end: NO EXCEPTION"); System.exit(1); }
+ catch (IndexOutOfBoundsException e) { System.out.println("VarHandle view at offset 36 of a 40-byte array: " + e.getClass().getSimpleName()); }
+ try (OffHeapLongs o = new FfmOffHeapLongs(4)) { o.get(4); System.out.println("ffm past end: NO EXCEPTION"); System.exit(1); }
+ catch (IndexOutOfBoundsException e) { System.out.println("FFM read of element 4 in a 4-element segment: " + e.getClass().getSimpleName()); }
+ OffHeapLongs closed = new FfmOffHeapLongs(4); closed.close();
+ try { closed.get(0); System.out.println("ffm after close: NO EXCEPTION"); System.exit(1); }
+ catch (IllegalStateException e) { System.out.println("FFM read after close: " + e.getClass().getSimpleName() + ": " + e.getMessage()); }
+
+ System.out.println("ALL " + checks + " CHECKS PASSED" + (newOnly ? " (new APIs only)" : ""));
+ }
+
+ // ---------- helpers ----------
+ static String names(int n) { return n == 2 ? "Unsafe and the replacement" : "the replacement alone"; }
+
+ static long fieldTotal(FieldCounter c) throws Exception {
+ Thread[] ts = new Thread[4];
+ for (int t = 0; t < ts.length; t++) { ts[t] = new Thread(() -> { for (int k = 0; k < 100_000; k++) c.getAndAdd(1); }); ts[t].start(); }
+ for (Thread t : ts) t.join();
+ return c.get();
+ }
+
+ static long[] fieldTrace(FieldCounter c) {
+ Random r = new Random(42);
+ long[] out = new long[2000];
+ for (int i = 0; i < out.length; i++) {
+ switch (r.nextInt(3)) {
+ case 0 -> c.set(r.nextInt(100));
+ case 1 -> out[i] = c.cas(r.nextInt(100), r.nextInt(100)) ? 1 : 0;
+ default -> out[i] = c.getAndAdd(r.nextInt(5));
+ }
+ out[i] = out[i] * 1_000_003L + c.get();
+ }
+ return out;
+ }
+
+ static long[] offHeapTrace(OffHeapLongs o) {
+ Random r = new Random(99);
+ long[] out = new long[2000];
+ for (int i = 0; i < out.length; i++) {
+ int idx = r.nextInt(o.length());
+ switch (r.nextInt(3)) {
+ case 0 -> o.set(idx, r.nextInt(1000));
+ case 1 -> out[i] = o.cas(idx, r.nextInt(1000), r.nextInt(1000)) ? 1 : 0;
+ default -> out[i] = o.getAndAdd(idx, r.nextInt(7));
+ }
+ out[i] = out[i] * 1_000_003L + o.get(idx);
+ }
+ return out;
+ }
+
+ static void same(String what, long[] a, long[] b) {
+ for (int i = 0; i < a.length; i++) if (a[i] != b[i]) { System.out.println("MISMATCH " + what + " at step " + i); System.exit(1); }
+ checks++;
+ }
+ static void require(String what, Object expected, Object actual) {
+ checks++;
+ if (!expected.equals(actual)) { System.out.println("MISMATCH " + what + ": expected " + expected + " got " + actual); System.exit(1); }
+ }
+}
diff --git a/unsafe-migration/src/FfmOffHeapLongs.java b/unsafe-migration/src/FfmOffHeapLongs.java
new file mode 100644
index 0000000..64916b2
--- /dev/null
+++ b/unsafe-migration/src/FfmOffHeapLongs.java
@@ -0,0 +1,29 @@
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+import java.lang.invoke.VarHandle;
+
+/** AFTER: the same array on java.lang.foreign (final in JDK 22). Bounds and lifetime are checked by the runtime. */
+public class FfmOffHeapLongs implements OffHeapLongs {
+ // access coordinates: (MemorySegment, long byteOffset); this VarHandle exposes get/set/volatile/CAS/getAndAdd modes
+ private static final VarHandle LONG = ValueLayout.JAVA_LONG.varHandle();
+ private final Arena arena = Arena.ofShared(); // shared: other threads may touch the segment; closing is thread-safe
+ private final MemorySegment seg;
+ private final int length;
+
+ public FfmOffHeapLongs(int length) {
+ this.length = length;
+ this.seg = arena.allocate(8L * length, 8); // zero-initialised, 8-byte aligned
+ }
+ public int length() { return length; }
+ public long get(int i) { return (long) LONG.getVolatile(seg, 8L * i); }
+ public void set(int i, long v) { LONG.setVolatile(seg, 8L * i, v); }
+ public boolean cas(int i, long e, long u) { return LONG.compareAndSet(seg, 8L * i, e, u); }
+ public long getAndAdd(int i, long d) { return (long) LONG.getAndAdd(seg, 8L * i, d); }
+ public void clear() { seg.fill((byte) 0); }
+ public void copyFrom(OffHeapLongs o) {
+ if (o instanceof FfmOffHeapLongs f) MemorySegment.copy(f.seg, 0, seg, 0, 8L * length); // one bulk copy, no loop
+ else for (int i = 0; i < length; i++) set(i, o.get(i));
+ }
+ public void close() { arena.close(); }
+}
diff --git a/unsafe-migration/src/FieldCounter.java b/unsafe-migration/src/FieldCounter.java
new file mode 100644
index 0000000..b7d1de2
--- /dev/null
+++ b/unsafe-migration/src/FieldCounter.java
@@ -0,0 +1,7 @@
+/** A volatile long field that several threads bump with compare-and-swap. Three implementations live in sibling files. */
+public interface FieldCounter {
+ long get();
+ void set(long v);
+ boolean cas(long expect, long update);
+ long getAndAdd(long delta);
+}
diff --git a/unsafe-migration/src/OffHeapLongs.java b/unsafe-migration/src/OffHeapLongs.java
new file mode 100644
index 0000000..a2099ce
--- /dev/null
+++ b/unsafe-migration/src/OffHeapLongs.java
@@ -0,0 +1,11 @@
+/** A fixed-size off-heap array of longs. Two implementations: Unsafe (before) and java.lang.foreign (after). */
+public interface OffHeapLongs extends AutoCloseable {
+ int length();
+ long get(int i);
+ void set(int i, long v);
+ boolean cas(int i, long expect, long update);
+ long getAndAdd(int i, long delta);
+ void clear();
+ void copyFrom(OffHeapLongs other);
+ @Override void close();
+}
diff --git a/unsafe-migration/src/UnsafeAccess.java b/unsafe-migration/src/UnsafeAccess.java
new file mode 100644
index 0000000..076c4a0
--- /dev/null
+++ b/unsafe-migration/src/UnsafeAccess.java
@@ -0,0 +1,17 @@
+import java.lang.reflect.Field;
+import sun.misc.Unsafe;
+
+/** The classic way every library gets an Unsafe: read the private static field "theUnsafe". */
+final class UnsafeAccess {
+ private UnsafeAccess() {}
+
+ static Unsafe get() {
+ try {
+ Field f = Unsafe.class.getDeclaredField("theUnsafe");
+ f.setAccessible(true);
+ return (Unsafe) f.get(null);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/unsafe-migration/src/UnsafeBytes.java b/unsafe-migration/src/UnsafeBytes.java
new file mode 100644
index 0000000..eb76a73
--- /dev/null
+++ b/unsafe-migration/src/UnsafeBytes.java
@@ -0,0 +1,11 @@
+import sun.misc.Unsafe;
+
+/** BEFORE: reading and writing a long at any offset of a byte[] with the "bimodal" Unsafe.getLong(Object, long). No bounds check. */
+public final class UnsafeBytes {
+ private UnsafeBytes() {}
+ private static final Unsafe U = UnsafeAccess.get();
+ private static final long BASE = U.arrayBaseOffset(byte[].class);
+
+ public static long get(byte[] a, int off) { return U.getLong(a, BASE + off); }
+ public static void put(byte[] a, int off, long v) { U.putLong(a, BASE + off, v); }
+}
diff --git a/unsafe-migration/src/UnsafeFieldCounter.java b/unsafe-migration/src/UnsafeFieldCounter.java
new file mode 100644
index 0000000..4c17987
--- /dev/null
+++ b/unsafe-migration/src/UnsafeFieldCounter.java
@@ -0,0 +1,17 @@
+import sun.misc.Unsafe;
+
+/** BEFORE: a CAS field the way pre-JDK-9 code did it. objectFieldOffset + compareAndSwapLong are deprecated for removal (JEP 471). */
+public class UnsafeFieldCounter implements FieldCounter {
+ private static final Unsafe U = UnsafeAccess.get();
+ private static final long VALUE;
+ static {
+ try { VALUE = U.objectFieldOffset(UnsafeFieldCounter.class.getDeclaredField("value")); }
+ catch (NoSuchFieldException e) { throw new ExceptionInInitializerError(e); }
+ }
+ private volatile long value;
+
+ public long get() { return value; }
+ public void set(long v) { value = v; }
+ public boolean cas(long expect, long update) { return U.compareAndSwapLong(this, VALUE, expect, update); }
+ public long getAndAdd(long delta) { return U.getAndAddLong(this, VALUE, delta); }
+}
diff --git a/unsafe-migration/src/UnsafeOffHeapLongs.java b/unsafe-migration/src/UnsafeOffHeapLongs.java
new file mode 100644
index 0000000..d53d4f4
--- /dev/null
+++ b/unsafe-migration/src/UnsafeOffHeapLongs.java
@@ -0,0 +1,28 @@
+import sun.misc.Unsafe;
+
+/** BEFORE: raw malloc through Unsafe. Every call below except the constructor's helper is deprecated for removal. */
+public class UnsafeOffHeapLongs implements OffHeapLongs {
+ private static final Unsafe U = UnsafeAccess.get();
+ private final long address;
+ private final int length;
+ private boolean closed;
+
+ public UnsafeOffHeapLongs(int length) {
+ this.length = length;
+ this.address = U.allocateMemory(8L * length);
+ U.setMemory(address, 8L * length, (byte) 0);
+ }
+ private long at(int i) {
+ if (closed) throw new IllegalStateException("closed"); // with Unsafe nobody checks this: a stale address is a JVM crash, not an exception
+ if (i < 0 || i >= length) throw new IndexOutOfBoundsException("index " + i);
+ return address + 8L * i;
+ }
+ public int length() { return length; }
+ public long get(int i) { return U.getLongVolatile(null, at(i)); }
+ public void set(int i, long v) { U.putLongVolatile(null, at(i), v); }
+ public boolean cas(int i, long e, long u) { return U.compareAndSwapLong(null, at(i), e, u); }
+ public long getAndAdd(int i, long d) { return U.getAndAddLong(null, at(i), d); }
+ public void clear() { U.setMemory(address, 8L * length, (byte) 0); }
+ public void copyFrom(OffHeapLongs o) { for (int i = 0; i < length; i++) set(i, o.get(i)); }
+ public void close() { if (!closed) { closed = true; U.freeMemory(address); } }
+}
diff --git a/unsafe-migration/src/VarHandleFieldCounter.java b/unsafe-migration/src/VarHandleFieldCounter.java
new file mode 100644
index 0000000..f6803b7
--- /dev/null
+++ b/unsafe-migration/src/VarHandleFieldCounter.java
@@ -0,0 +1,17 @@
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+
+/** AFTER: the same field with a VarHandle (JEP 193, JDK 9). No Unsafe anywhere. */
+public class VarHandleFieldCounter implements FieldCounter {
+ private static final VarHandle VALUE;
+ static {
+ try { VALUE = MethodHandles.lookup().findVarHandle(VarHandleFieldCounter.class, "value", long.class); }
+ catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
+ }
+ private volatile long value;
+
+ public long get() { return value; }
+ public void set(long v) { value = v; }
+ public boolean cas(long expect, long update) { return VALUE.compareAndSet(this, expect, update); }
+ public long getAndAdd(long delta) { return (long) VALUE.getAndAdd(this, delta); }
+}
diff --git a/unsafe-migration/src/ViewBytes.java b/unsafe-migration/src/ViewBytes.java
new file mode 100644
index 0000000..2efed19
--- /dev/null
+++ b/unsafe-migration/src/ViewBytes.java
@@ -0,0 +1,12 @@
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteOrder;
+
+/** AFTER: the same access through a byte-array-view VarHandle (JDK 9). Bounds-checked; native byte order to match Unsafe. */
+public final class ViewBytes {
+ private ViewBytes() {}
+ private static final VarHandle VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.nativeOrder());
+
+ public static long get(byte[] a, int off) { return (long) VIEW.get(a, off); }
+ public static void put(byte[] a, int off, long v) { VIEW.set(a, off, v); }
+}
diff --git a/unsafe-migration/tools/GuavaProbe.java b/unsafe-migration/tools/GuavaProbe.java
new file mode 100644
index 0000000..4c5d14b
--- /dev/null
+++ b/unsafe-migration/tools/GuavaProbe.java
@@ -0,0 +1,18 @@
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.UnsignedBytes;
+import com.google.common.util.concurrent.SettableFuture;
+import java.util.Comparator;
+
+/** Three Guava features whose implementations have an Unsafe fast path (see output/11): hashing, byte comparison, futures. Needs guava on the class path. */
+public class GuavaProbe {
+ public static void main(String[] args) throws Exception {
+ byte[] data = "unsafe-migration".getBytes();
+ System.out.println("murmur3_128 = " + Hashing.murmur3_128().hashBytes(data));
+ Comparator cmp = UnsignedBytes.lexicographicalComparator();
+ System.out.println("comparator = " + cmp.getClass().getName());
+ System.out.println("compare = " + Integer.signum(cmp.compare(new byte[] {1, 2, 3}, new byte[] {1, 2, 4})));
+ SettableFuture f = SettableFuture.create();
+ f.set("ok");
+ System.out.println("future = " + f.get());
+ }
+}
diff --git a/unsafe-migration/tools/ListUnsafe.java b/unsafe-migration/tools/ListUnsafe.java
new file mode 100644
index 0000000..be704c1
--- /dev/null
+++ b/unsafe-migration/tools/ListUnsafe.java
@@ -0,0 +1,24 @@
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.*;
+
+/** Asks the running JDK which public sun.misc.Unsafe methods are deprecated for removal. Run: java tools/ListUnsafe.java */
+public class ListUnsafe {
+ public static void main(String[] args) throws Exception {
+ List kept = new ArrayList<>();
+ int removal = 0, total = 0;
+ for (Method m : Class.forName("sun.misc.Unsafe").getDeclaredMethods()) {
+ if (!Modifier.isPublic(m.getModifiers())) continue;
+ total++;
+ Deprecated d = m.getAnnotation(Deprecated.class);
+ if (d != null && d.forRemoval()) removal++;
+ else kept.add(m.getName() + (d != null ? " (deprecated, not for removal)" : ""));
+ }
+ Collections.sort(kept);
+ System.out.println("java.version=" + System.getProperty("java.version"));
+ System.out.println("public methods declared by sun.misc.Unsafe: " + total);
+ System.out.println("deprecated for removal: " + removal);
+ System.out.println("not deprecated for removal: " + kept.size());
+ kept.forEach(k -> System.out.println(" " + k));
+ }
+}
diff --git a/unsafe-migration/tools/NettyProbe.java b/unsafe-migration/tools/NettyProbe.java
new file mode 100644
index 0000000..b7e61c5
--- /dev/null
+++ b/unsafe-migration/tools/NettyProbe.java
@@ -0,0 +1,15 @@
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.util.internal.PlatformDependent;
+
+/** A real third-party library that uses Unsafe: allocate one direct Netty buffer and use it. Compile and run with netty-common + netty-buffer on the class path. */
+public class NettyProbe {
+ public static void main(String[] args) {
+ System.out.println("netty sees Unsafe: " + PlatformDependent.hasUnsafe());
+ ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.directBuffer(16);
+ buf.writeLong(0xCAFEBABEL);
+ System.out.println("direct=" + buf.isDirect() + " hasMemoryAddress=" + buf.hasMemoryAddress() + " readLong=" + Long.toHexString(buf.readLong()));
+ buf.release();
+ System.out.println("done");
+ }
+}
diff --git a/unsafe-migration/tools/Prop.java b/unsafe-migration/tools/Prop.java
new file mode 100644
index 0000000..8644fd6
--- /dev/null
+++ b/unsafe-migration/tools/Prop.java
@@ -0,0 +1,2 @@
+/** Prints the system property the JVM sets from --sun-misc-unsafe-memory-access (libraries can read it). */
+public class Prop { public static void main(String[] a){ System.out.println("sun.misc.unsafe.memory.access=" + System.getProperty("sun.misc.unsafe.memory.access")); } }
diff --git a/unsafe-migration/tools/ScanJar.java b/unsafe-migration/tools/ScanJar.java
new file mode 100644
index 0000000..b52f9d8
--- /dev/null
+++ b/unsafe-migration/tools/ScanJar.java
@@ -0,0 +1,97 @@
+import java.io.InputStream;
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.CodeModel;
+import java.lang.classfile.MethodModel;
+import java.lang.classfile.instruction.InvokeInstruction;
+import java.lang.classfile.instruction.ConstantInstruction;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * Static scan of jars for call sites of the sun.misc.Unsafe methods that are deprecated for removal (JEP 471).
+ *
+ * It reads bytecode with the java.lang.classfile API (final in JDK 24). It does NOT run anything, so it reports
+ * "this jar contains a call to X", never "this call is reached at run time". Which methods count as
+ * "memory-access" is decided by asking the running JDK: a method is counted when its own Unsafe overload carries
+ * @Deprecated(forRemoval = true).
+ *
+ * usage: java ScanJar.java [--classes] jar [jar ...]
+ */
+public class ScanJar {
+ static final String UNSAFE = "sun/misc/Unsafe";
+
+ public static void main(String[] args) throws Exception {
+ boolean perClass = false;
+ List jars = new ArrayList<>();
+ for (String a : args) { if (a.equals("--classes")) perClass = true; else jars.add(a); }
+
+ Set removal = new HashSet<>(); // name + descriptor of every forRemoval overload
+ int total = 0;
+ for (Method m : Class.forName("sun.misc.Unsafe").getDeclaredMethods()) {
+ if (!java.lang.reflect.Modifier.isPublic(m.getModifiers())) continue;
+ total++;
+ Deprecated d = m.getAnnotation(Deprecated.class);
+ if (d != null && d.forRemoval()) {
+ removal.add(m.getName() + MethodType.methodType(m.getReturnType(), m.getParameterTypes()).descriptorString());
+ }
+ }
+ System.out.println("this JDK: sun.misc.Unsafe has " + total + " public methods, " + removal.size() + " deprecated for removal");
+
+ for (String jar : jars) scan(jar, removal, perClass);
+ }
+
+ static void scan(String jar, Set removal, boolean perClass) throws Exception {
+ int classes = 0, refClasses = 0, callSites = 0, mrClasses = 0, stringRefs = 0;
+ Map methods = new TreeMap<>();
+ Map perClassCalls = new TreeMap<>();
+ Set otherUnsafeCalls = new TreeSet<>();
+ Set mrCallers = new TreeSet<>();
+ try (JarFile jf = new JarFile(jar)) {
+ for (JarEntry e : Collections.list(jf.entries())) {
+ String n = e.getName();
+ if (!n.endsWith(".class") || n.endsWith("module-info.class")) continue;
+ boolean mr = n.startsWith("META-INF/versions/");
+ byte[] bytes;
+ try (InputStream in = jf.getInputStream(e)) { bytes = in.readAllBytes(); }
+ var cm = ClassFile.of().parse(bytes);
+ classes++;
+ boolean ref = false; int calls = 0;
+ for (MethodModel mm : cm.methods()) {
+ CodeModel code = mm.code().orElse(null);
+ if (code == null) continue;
+ for (var el : code) {
+ if (el instanceof InvokeInstruction ii && ii.owner().asInternalName().equals(UNSAFE)) {
+ ref = true;
+ String key = ii.name().stringValue() + ii.typeSymbol().descriptorString();
+ if (removal.contains(key)) { calls++; methods.merge(ii.name().stringValue(), 1, Integer::sum); }
+ else otherUnsafeCalls.add(ii.name().stringValue());
+ } else if (el instanceof ConstantInstruction.LoadConstantInstruction lc
+ && lc.constantEntry().constantValue() instanceof String s
+ && (s.equals("sun.misc.Unsafe") || s.equals("theUnsafe"))) {
+ stringRefs++; ref = true;
+ }
+ }
+ }
+ if (ref) refClasses++;
+ if (calls > 0) {
+ callSites += calls;
+ String cn = n.substring(0, n.length() - 6).replace('/', '.');
+ perClassCalls.put(cn, calls);
+ if (mr) { mrClasses++; mrCallers.add(cn); }
+ }
+ }
+ }
+ String name = jar.substring(jar.lastIndexOf('/') + 1);
+ System.out.println();
+ System.out.println(name + ": " + classes + " classes scanned");
+ System.out.println(" classes touching sun.misc.Unsafe (call, or the strings 'sun.misc.Unsafe'/'theUnsafe'): " + refClasses);
+ System.out.println(" classes calling a removal-deprecated Unsafe method: " + perClassCalls.size() + " (" + callSites + " call sites, " + methods.size() + " distinct methods)");
+ if (!methods.isEmpty()) System.out.println(" methods: " + methods);
+ if (!otherUnsafeCalls.isEmpty()) System.out.println(" other (not deprecated for removal) Unsafe methods called: " + otherUnsafeCalls);
+ if (!mrCallers.isEmpty()) System.out.println(" of those classes, under META-INF/versions: " + mrCallers.size());
+ if (perClass) perClassCalls.forEach((k, v) -> System.out.println(" " + k + " " + v));
+ }
+}