This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 751f29f12933 [SPARK-57877][CORE] DirectByteBuffer cleaner support Java
26
751f29f12933 is described below
commit 751f29f12933ef3626518e8ed7a13a225c37d326
Author: Cheng Pan <[email protected]>
AuthorDate: Tue Jul 21 23:22:38 2026 -0700
[SPARK-57877][CORE] DirectByteBuffer cleaner support Java 26
### What changes were proposed in this pull request?
[JDK-8344332](https://bugs.openjdk.org/browse/JDK-8344332) migrates
`DirectByteBuffer` away from `jdk.internal.ref.Cleaner` to a new
`java.nio.BufferCleaner`, and
[JDK-8361426](https://bugs.openjdk.org/browse/JDK-8361426) removes
`jdk.internal.ref.Cleaner` entirely in JDK 26.
### Why are the changes needed?
Adapt to upstream JDK change.
Close #56939
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
#### Run PlatformUtilSuite with JDK 26
```
JAVA_HOME=/path/of/openjdk-26 build/sbt "unsafe/testOnly *PlatformUtilSuite"
```
before
```
[info] Test run started (JUnit Jupiter)
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#freeingOnHeapMemoryBlockResetsBaseObjectAndOffset()
started
[error] Test
org.apache.spark.unsafe.PlatformUtilSuite.freeingOnHeapMemoryBlockResetsBaseObjectAndOffset
failed: java.lang.ExceptionInInitializerError: null, took 0.022s
[error] at
org.apache.spark.unsafe.memory.HeapMemoryAllocator.allocate(HeapMemoryAllocator.java:73)
[error] at
org.apache.spark.unsafe.PlatformUtilSuite.freeingOnHeapMemoryBlockResetsBaseObjectAndOffset(PlatformUtilSuite.java:79)
[error] Caused by: java.lang.IllegalStateException:
java.lang.ClassNotFoundException: jdk.internal.ref.Cleaner
[error] at org.apache.spark.unsafe.Platform.<clinit>(Platform.java:104)
[error] ... 2 more
[error] Caused by: java.lang.ClassNotFoundException:
jdk.internal.ref.Cleaner
[error] at
jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
[error] at java.lang.ClassLoader.loadClass(ClassLoader.java:502)
[error] at java.lang.Class.forName0(Native Method)
[error] at java.lang.Class.forName(Class.java:478)
[error] at java.lang.Class.forName(Class.java:468)
[error] at org.apache.spark.unsafe.Platform.<clinit>(Platform.java:84)
[error] ... 2 more
...
```
after
```
[info] Test run started (JUnit Jupiter)
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#freeingOnHeapMemoryBlockResetsBaseObjectAndOffset()
started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#overlappingCopyMemory() started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#memoryDebugFillEnabledInTest() started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#offHeapMemoryAllocatorThrowsAssertionErrorOnDoubleFree()
started
[info] Test org.apache.spark.unsafe.PlatformUtilSuite#heapMemoryReuse()
started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#onHeapMemoryAllocatorPoolingReUsesLongArrays()
started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#onHeapMemoryAllocatorThrowsAssertionErrorOnDoubleFree()
started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#freeingOffHeapMemoryBlockResetsOffset()
started
[info] Test
org.apache.spark.unsafe.PlatformUtilSuite#cleanerCreateMethodIsDefined() started
[info] Test run finished: 0 failed, 0 ignored, 9 total, 0.104s
[info] Passed: Total 9, Failed 0, Errors 0, Passed 9
[success] Total time: 9 s, completed Jul 2, 2026, 2:56:53 PM
```
#### Manually verify with JPMS args
```
cat > /tmp/CheckPlatform.java <<EOF
public class CheckPlatform {
public static void main(String[] a) {
int m =
Integer.parseInt(System.getProperty("java.specification.version"));
Throwable err = null; boolean def = false;
try { def =
org.apache.spark.unsafe.Platform.cleanerCreateMethodIsDefined(); }
catch (Throwable t) { err = t; }
String s = "JDK " + m + " | cleanerCreateMethodIsDefined=" + def;
if (err != null) { Throwable c = err.getCause() != null ?
err.getCause() : err;
s += " | init threw " + err.getClass().getSimpleName() + " <- " +
c.getClass().getName() + ": " + c.getMessage(); }
System.out.println(s);
}
}
EOF
```
```
build/sbt 'unsafe/package'
```
```
NIO='--add-opens=java.base/java.nio=ALL-UNNAMED'
REF='--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED'
```
master branch
```
printf '%-46s ' "JDK25 / no add-opens";
"$JAVA25_HOME/bin/java" -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK25 / java.nio + jdk.internal.ref";
"$JAVA25_HOME/bin/java" $NIO $REF -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK26 / no add-opens";
"$JAVA26_HOME/bin/java" -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK26 / java.nio + jdk.internal.ref";
"$JAVA26_HOME/bin/java" $NIO $REF -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
```
```
JDK25 / no add-opens JDK 25 |
cleanerCreateMethodIsDefined=false
JDK25 / java.nio + jdk.internal.ref JDK 25 |
cleanerCreateMethodIsDefined=true
JDK26 / no add-opens JDK 26 |
cleanerCreateMethodIsDefined=false
JDK26 / java.nio + jdk.internal.ref JDK 26 |
cleanerCreateMethodIsDefined=false | init threw ExceptionInInitializerError <-
java.lang.IllegalStateException: java.lang.ClassNotFoundException:
jdk.internal.ref.Cleaner
```
this PR
```
printf '%-46s ' "JDK25 / no add-opens";
"$JAVA25_HOME/bin/java" -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK25 / java.nio only";
"$JAVA25_HOME/bin/java" $NIO -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK25 / java.nio + jdk.internal.ref";
"$JAVA25_HOME/bin/java" $NIO $REF -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK26 / no add-opens";
"$JAVA26_HOME/bin/java" -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
printf '%-46s ' "JDK26 / java.nio only";
"$JAVA26_HOME/bin/java" $NIO -cp
common/unsafe/target/scala-2.13/spark-unsafe_2.13-5.0.0-SNAPSHOT.jar
/tmp/CheckPlatform.java 2>/dev/null
```
```
JDK25 / no add-opens JDK 25 |
cleanerCreateMethodIsDefined=false
JDK25 / java.nio only JDK 25 |
cleanerCreateMethodIsDefined=false
JDK25 / java.nio + jdk.internal.ref JDK 25 |
cleanerCreateMethodIsDefined=true
JDK26 / no add-opens JDK 26 |
cleanerCreateMethodIsDefined=false
JDK26 / java.nio only JDK 26 |
cleanerCreateMethodIsDefined=true
```
### Was this patch authored or co-authored using generative AI tooling?
Yes, GLM 5.2 is used to construct manual test cases.
Closes #56954 from pan3793/SPARK-57877.
Authored-by: Cheng Pan <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../java/org/apache/spark/unsafe/Platform.java | 28 +++++++++++++++-------
1 file changed, 19 insertions(+), 9 deletions(-)
diff --git a/common/unsafe/src/main/java/org/apache/spark/unsafe/Platform.java
b/common/unsafe/src/main/java/org/apache/spark/unsafe/Platform.java
index 9bb69ec2471b..efd9cc70d01c 100644
--- a/common/unsafe/src/main/java/org/apache/spark/unsafe/Platform.java
+++ b/common/unsafe/src/main/java/org/apache/spark/unsafe/Platform.java
@@ -81,16 +81,26 @@ public final class Platform {
// no point continuing if the above failed:
if (DBB_CONSTRUCTOR != null && DBB_CLEANER_FIELD != null) {
- Class<?> cleanerClass = Class.forName("jdk.internal.ref.Cleaner");
- Method createMethod = cleanerClass.getMethod("create", Object.class,
Runnable.class);
- // Accessing jdk.internal.ref.Cleaner should actually fail by default
in JDK 9+,
- // unfortunately, unless the user has allowed access with something
like
- // --add-opens java.base/jdk.internal.ref=ALL-UNNAMED If not, we
can't use the Cleaner
- // hack below. It doesn't break, just means the user might run into
the default JVM limit
- // on off-heap memory and increase it or set the flag above. This
tests whether it's
- // available:
+ // JDK-8344332 (JDK 26) migrate DirectByteBuffer away from
jdk.internal.ref.Cleaner
+ Class<?> cleanerClass = (majorVersion < 26) ?
+ Class.forName("jdk.internal.ref.Cleaner") :
+ Class.forName("java.nio.BufferCleaner");
+ Method createMethod = (majorVersion < 26) ?
+ cleanerClass.getMethod("create", Object.class, Runnable.class) :
+ cleanerClass.getMethod("register", Object.class, Runnable.class);
+ // Accessing jdk.internal.ref.Cleaner (JDK 9~25) /
java.nio.BufferCleaner (JDK 26+)
+ // should actually fail by default, unless the user has allowed access
with something like
+ // --add-opens java.base/jdk.internal.ref=ALL-UNNAMED (JDK 9~25)
+ // --add-opens java.base/java.nio=ALL-UNNAMED (JDK 26+)
+ // If not, we can't use the Cleaner hack below. It doesn't break, just
means the user might
+ // run into the default JVM limit on off-heap memory and increase it
or set the flag above.
+ // This tests whether it's available:
try {
- createMethod.invoke(null, null, null);
+ if (createMethod.trySetAccessible()) {
+ createMethod.invoke(null, new Object(), (Runnable) () -> {});
+ } else {
+ createMethod = null;
+ }
} catch (IllegalAccessException e) {
// Don't throw an exception, but can't log here?
createMethod = null;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]