PAYATHON 2026

Java 加载 DLL 时出现 UnsatisfiedLinkError

排障赵师傅

结论

出现这类错误时,目标 DLL 可能存在,真正加载失败的是它依赖的其他 DLL。System.load(...) 加载目标文件后,Windows 还会继续加载相关依赖。依赖缺失、架构不匹配、运行库未安装或 DLL 初始化失败,都可能导致 Java 抛出 UnsatisfiedLinkError。此时的错误信息仍可能是 “Can’t load library” 或 “The specified module could not be found”。

从示例来看,首先要检查这段硬编码路径:

System.load("C:/Users/foru/CLionProjects/CheckerGameLibrary/cmake-build-debug/libCheckerGameLibrary.dll");

它指向开发机器上的 CLion 构建目录。程序复制到另一台设备后,这个目录通常不存在。即使目录存在,里面也可能只有主 DLL,没有相关依赖。IntelliJ 中的配置只对相应的运行配置生效,不会自动安装 DLL、C++ 运行库或其他本地依赖。

先确认 Java 实际加载的文件

System.load 要求传入 DLL 的绝对路径。不要在代码中写死开发机器的用户名和构建目录,可以先用以下代码检查路径:

import java.nio.file.Files;
import java.nio.file.Path;

public class NativeLoader {
    static {
        Path dll = Path.of(
                "native",
                "libCheckerGameLibrary.dll"
        ).toAbsolutePath().normalize();

        System.out.println("DLL path: " + dll);
        System.out.println("Exists: " + Files.exists(dll));
        System.out.println("Regular file: " + Files.isRegularFile(dll));
        System.out.println("Readable: " + Files.isReadable(dll));
        System.out.println("JVM architecture: " + System.getProperty("os.arch"));
        System.out.println("Java home: " + System.getProperty("java.home"));

        System.load(dll.toString());
    }
}

应用从不同的工作目录启动时,相对路径对应的位置也会变化。可以同时输出当前工作目录:

System.out.println("Working directory: " + System.getProperty("user.dir"));

发布时,建议把 DLL 放在由应用统一管理的目录中,例如:

checker-game/
├─ app.jar
└─ native/
   ├─ libCheckerGameLibrary.dll
   └─ 其他依赖 DLL

再通过启动脚本固定工作目录,或使用配置项传入本地库目录。

System.load 和 System.loadLibrary 的区别

两者处理路径的方式不同。

使用绝对路径加载

System.load("C:\\checker-game\\native\\libCheckerGameLibrary.dll");

System.load 会直接加载指定文件,不通过 java.library.path 查找主 DLL。

按库名搜索

System.loadLibrary("CheckerGameLibrary");

使用这种方式时,不要填写目录或 .dll 后缀。JVM 会根据 java.library.path 搜索库文件。启动时可以这样指定路径:

java -Djava.library.path="C:\checker-game\native" -jar app.jar

以下代码可以查看当前搜索路径:

System.out.println(System.getProperty("java.library.path"));

java.library.path 只负责帮助 JVM 找到主 DLL。主 DLL 的其他依赖仍由 Windows 加载器查找。即使主 DLL 使用绝对路径加载,也不代表它的所有依赖都能自动找到。

DLL 存在但仍然无法加载的常见原因

1. 依赖 DLL 缺失

假设 libCheckerGameLibrary.dll 依赖以下文件:

libstdc++-6.dll
libgcc_s_seh-1.dll
winpthread-1.dll
SomeThirdParty.dll

如果开发环境使用 MinGW、MSYS2 或第三方 C++ 库,这些依赖可能只安装在开发机器上。只把主 DLL 复制到新设备,程序仍然无法运行。

使用 Microsoft Visual C++ 编译时,程序也可能需要对应架构和版本的 Microsoft Visual C++ Redistributable。具体需要哪套运行库,应根据 DLL 的实际依赖和构建工具链判断,不能只看文件名。

2. 间接依赖的架构不匹配

主 DLL 和 Java 都是 64 位,只能确认第一层架构一致。主 DLL 依赖的每个本地库也必须采用兼容架构。

例如,以下组合仍会加载失败:

64-bit JVM
  └─ 64-bit libCheckerGameLibrary.dll
       └─ 32-bit ThirdParty.dll

Windows 常见的相关提示包括 %1 is not a valid Win32 application,通常表示架构不匹配或文件格式有误。

3. Windows 找不到依赖库

Windows 会按照系统的 DLL 搜索规则查找依赖,具体行为还受 Windows 版本、应用清单和加载方式影响。可以按以下方式排查:

  • 将应用的私有依赖放到可执行程序所在目录或明确的本地库目录中。
  • 启动 Java 进程前,将依赖目录加入 PATH。
  • 修改环境变量后,重新启动终端和 IntelliJ。已经运行的进程不会自动获得新的环境变量。
  • 不要依赖 CLion、MSYS2 或 Visual Studio 安装目录中偶然存在的 DLL。

可以在 PowerShell 中临时设置:

$env:Path = "C:\checker-game\native;$env:Path"
java -jar app.jar

如果应用只能在 IDE 中运行,需要比较 IDE 和命令行进程的 PATH、工作目录及所用 JDK。

4. 文件被安全策略阻止

从网络或其他设备复制过来的 DLL,可能受到 Windows 安全标记、杀毒软件、应用控制策略或目录权限的限制。需要检查:

  • 当前用户对 DLL 及其目录是否有读取和执行权限;
  • DLL 是否已被隔离或删除;
  • 文件属性中是否有“解除锁定”选项;
  • Windows 事件日志或安全软件日志中是否有拦截记录。

不要通过关闭系统安全功能进行验证。应先查看拦截记录,只对确认可信的构建产物解除限制。

5. DLL 初始化失败

所有文件都能找到时,DLL 的 DllMain 或静态 C++ 初始化代码仍可能失败。例如,初始化代码读取了开发机器上的配置,访问了不存在的目录,或尝试加载另一个插件。

遇到这种情况,需要查看 Windows 加载器返回的具体错误,并使用进程监控工具或本地调试器继续定位。

如何检查 DLL 的依赖项

如果已经安装 Visual Studio 或 Build Tools,可以在对应的开发者命令提示符中运行:

dumpbin /DEPENDENTS libCheckerGameLibrary.dll

查看文件头和目标架构:

dumpbin /HEADERS libCheckerGameLibrary.dll

/DEPENDENTS 会列出导入表中的直接依赖,但未必能直接指出新设备上缺少哪个文件。也可以使用 Dependencies 工具检查递归依赖、架构和无法解析的模块。

如果静态检查没有发现问题,可以使用 Process Monitor:

  1. 按 Java 进程名过滤,例如 java.exe 或 javaw.exe。
  2. 按操作类型或结果进一步过滤 DLL 文件访问。
  3. 运行程序,观察 NAME NOT FOUND、PATH NOT FOUND 或 ACCESS DENIED。
  4. 检查主 DLL 加载前后有哪些 DLL 被连续查找但始终失败。

Process Monitor 会显示大量正常的路径探测记录。某个路径出现 NAME NOT FOUND 未必代表故障,还要确认同一个 DLL 后续是否在其他目录中成功找到。

确认 JVM 和 DLL 架构

应查看当前实际运行的 Java,而不只是 IntelliJ 中的项目设置:

java -version
where java

也可以输出 JVM 属性:

System.out.println(System.getProperty("java.version"));
System.out.println(System.getProperty("java.vendor"));
System.out.println(System.getProperty("java.home"));
System.out.println(System.getProperty("os.arch"));

os.arch 可以用于初步判断。检查 PE 文件架构时,应以 dumpbin /HEADERS 或其他 PE 分析工具的结果为准。

区分“DLL 加载失败”和“JNI 方法找不到”

主 DLL 成功加载后,如果 JNI 导出函数的名称或签名不匹配,通常会在调用本地方法时看到类似错误:

java.lang.UnsatisfiedLinkError:
'long main.Main.nativeCreate()'

或者:

java.lang.UnsatisfiedLinkError:
no nativeCreate in java.library.path

这和当前的 Can't load library 发生在不同阶段。出现前一种问题时,需要检查 JNI 函数导出、包名、类名和方法签名,例如:

extern "C"
JNIEXPORT jlong JNICALL
Java_main_Main_nativeCreate(JNIEnv* env, jobject object) {
    // ...
}

可以通过以下命令查看 DLL 导出的符号:

dumpbin /EXPORTS libCheckerGameLibrary.dll

使用 C++ 编译 JNI 函数时,通常需要添加 extern "C",否则 C++ 名称修饰可能导致 JVM 找不到预期的导出符号。更可靠的做法是使用 javac -h 根据 Java 声明生成 JNI 头文件,再让 C++ 实现严格匹配头文件中的声明。

建议的排查顺序

  1. 删除代码中指向开发机器的绝对路径,确认新设备上的实际 DLL 路径。
  2. 使用 Files.exists、Files.isRegularFile 和 Files.isReadable 输出检查结果。
  3. 确认 IntelliJ 和命令行使用的是同一个 JDK。
  4. 检查 JVM、主 DLL 和所有间接依赖的架构。
  5. 使用 dumpbin /DEPENDENTS 或 Dependencies 查看依赖链。
  6. 安装构建该 DLL 所需的 C/C++ 运行库,或随应用部署允许再分发的私有依赖。
  7. 使用 Process Monitor 查找缺失、路径错误或被拒绝访问的 DLL。
  8. 主 DLL 成功加载后,如果调用 nativeCreate() 等方法仍然报错,再检查 JNI 导出名称和方法签名。

就这个案例而言,最可能的原因是代码仍在引用开发机器上的 CLion 构建路径,或者新设备上只有 libCheckerGameLibrary.dll,缺少它依赖的 C++ 运行库及第三方 DLL。建议先检查完整依赖链,再考虑调整 IntelliJ 的 Library 设置。

备注:内容仅供参考。