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)); + } +}