Skip to content

Java 17 运行 Apache Spark 的 "sun.nio.ch.DirectBuffer" 访问错误解决方案

问题描述

在 Java 17 环境中运行 Apache Spark 3.3.0 及以上版本时,可能会遇到以下错误:

java.lang.IllegalAccessError: class org.apache.spark.storage.StorageUtils$ cannot access class sun.nio.ch.DirectBuffer 
(in module java.base) because module java.base does not export sun.nio.ch to unnamed module

这个错误通常发生在尝试创建 SparkSession 或执行 Spark 操作时,原因是 Java 17 的模块系统加强了访问控制,而 Spark 使用了部分内部 API(如 sun.nio.ch.DirectBuffer)。

解决方案

以下是针对不同环境和构建工具的解决方案:

通用 JVM 参数方案

最直接的解决方案是添加必要的 JVM 参数来开放内部模块的访问权限:

bash
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.io=ALL-UNNAMED
--add-opens=java.base/java.net=ALL-UNNAMED
--add-opens=java.base/java.nio=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
--add-opens=java.base/sun.nio.cs=ALL-UNNAMED
--add-opens=java.base/sun.security.action=ALL-UNNAMED
--add-opens=java.base/sun.util.calendar=ALL-UNNAMED
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED

注意

虽然只需 --add-opens=java.base/sun.nio.ch=ALL-UNNAMED 就能解决直接错误,但建议添加全套参数以确保 Spark 其他功能正常工作。

Maven 项目配置

运行应用程序

.mvn/jvm.config 文件中添加:

--add-opens=java.base/sun.nio.ch=ALL-UNNAMED

运行测试

pom.xml 中配置 Maven Surefire 插件:

xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens=java.base/java.lang=ALL-UNNAMED
            --add-opens=java.base/java.lang.invoke=ALL-UNNAMED
            --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
            --add-opens=java.base/java.io=ALL-UNNAMED
            --add-opens=java.base/java.net=ALL-UNNAMED
            --add-opens=java.base/java.nio=ALL-UNNAMED
            --add-opens=java.base/java.util=ALL-UNNAMED
            --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
            --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED
            --add-opens=java.base/sun.nio.ch=ALL-UNNAMED
            --add-opens=java.base/sun.nio.cs=ALL-UNNAMED
            --add-opens=java.base/sun.security.action=ALL-UNNAMED
            --add-opens=java.base/sun.util.calendar=ALL-UNNAMED
            --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED
        </argLine>
    </configuration>
</plugin>

Gradle 项目配置

运行应用程序

build.gradle 中:

groovy
application {
    applicationDefaultJvmArgs = [
        "--add-opens=java.base/java.lang=ALL-UNNAMED",
        "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED",
        "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
        "--add-opens=java.base/java.io=ALL-UNNAMED",
        "--add-opens=java.base/java.net=ALL-UNNAMED",
        "--add-opens=java.base/java.nio=ALL-UNNAMED",
        "--add-opens=java.base/java.util=ALL-UNNAMED",
        "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED",
        "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED",
        "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
        "--add-opens=java.base/sun.nio.cs=ALL-UNNAMED",
        "--add-opens=java.base/sun.security.action=ALL-UNNAMED",
        "--add-opens=java.base/sun.util.calendar=ALL-UNNAMED",
        "--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED"
    ]
}

运行测试

build.gradle.kts 中:

kotlin
tasks.test {
    val sparkJava17CompatibleJvmArgs = listOf(
        "--add-opens=java.base/java.lang=ALL-UNNAMED",
        "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED",
        "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
        "--add-opens=java.base/java.io=ALL-UNNAMED",
        "--add-opens=java.base/java.net=ALL-UNNAMED",
        "--add-opens=java.base/java.nio=ALL-UNNAMED",
        "--add-opens=java.base/java.util=ALL-UNNAMED",
        "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED",
        "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED",
        "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
        "--add-opens=java.base/sun.nio.cs=ALL-UNNAMED",
        "--add-opens=java.base/sun.security.action=ALL-UNNAMED",
        "--add-opens=java.base/sun.util.calendar=ALL-UNNAMED",
        "--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED"
    )
    jvmArgs = sparkJava17CompatibleJvmArgs
}

IDE 配置

IntelliJ IDEA

  1. 打开运行/调试配置
  2. 在 "VM options" 字段中添加:
    --add-opens=java.base/sun.nio.ch=ALL-UNNAMED
完整 VM 选项(推荐)
--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.nio.cs=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED --add-opens=java.base/sun.util.calendar=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED

Visual Studio Code

.vscode/launch.json 中添加:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "java",
      "name": "Main",
      "request": "launch",
      "mainClass": "com.spark.Main",
      "vmArgs": "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"
    }
  ]
}

环境变量方案

Docker 环境

在 Dockerfile 中添加:

dockerfile
ENV JDK_JAVA_OPTIONS="--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"

命令行环境

bash
export JAVA_OPTS='--add-exports java.base/sun.nio.ch=ALL-UNNAMED'

SBT 项目配置

build.sbt 中添加:

scala
Test / javaOptions ++= Seq(
  "base/java.lang", "base/java.lang.invoke", "base/java.lang.reflect", "base/java.io", "base/java.net", "base/java.nio",
  "base/java.util", "base/java.util.concurrent", "base/java.util.concurrent.atomic",
  "base/sun.nio.ch", "base/sun.nio.cs", "base/sun.security.action",
  "base/sun.util.calendar", "security.jgss/sun.security.krb5",
).map("--add-opens=java." + _ + "=ALL-UNNAMED")

或者在项目根目录创建 .jvmopts 文件:

--add-exports java.base/sun.nio.ch=ALL-UNNAMED

替代方案

如果无法添加 JVM 参数(如在某些云端环境中),可以考虑以下替代方案:

  1. 降级到 Java 11:这是最稳定的解决方案,因为 Spark 对 Java 11 的支持更加成熟
  2. 升级 Spark 版本:Spark 3.3.2+ 版本对 Java 17 的兼容性有所改善

技术背景

这个问题的根本原因是 Java 9 引入的模块系统(JPMS)。在 Java 17 中,对内部 API 的访问控制更加严格,而 Spark 使用了 sun.nio.ch.DirectBuffer 等内部类来进行高性能 I/O 操作。

--add-opens 参数告诉 JVM 允许特定模块向未命名模块(如 Spark)开放指定的包,从而绕过模块系统的访问限制。

兼容性说明

虽然 Spark 官方文档声明支持 Java 17,但在实际使用中可能需要额外的配置。建议在生产环境中使用 Java 11,或等待 Spark 后续版本对 Java 17 更完善的支持。

总结

Java 17 运行 Apache Spark 时的 "sun.nio.ch.DirectBuffer" 访问错误可以通过添加适当的 JVM 参数来解决。根据您的开发环境和构建工具选择合适的配置方式,确保 Spark 能够正常访问所需的内部 API。

对于生产环境,建议评估使用 Java 11 的可行性,或者密切关注 Spark 社区对 Java 17 支持的后续改进。