加载使用本机代码的 Java 类 的多个版本

Loading multiple versions of Java classes that use native code

如果您想加载一个 class 的多个版本,如果它们实现共享接口并且位于单独的 JAR 中,您可以这样做,using a separate class loader for each version

如果您有调用本机代码的 JAR,则可以将本机代码的共享库 (DLL) 存储在其 JAR by extracting the shared library to a temporary file and then using System.load to load the library from the temporary file

但是如果你两者都做,会成功吗?如果 JAR 的两个版本都调用本机代码,并且都包含不同版本的共享库,会发生什么情况?

让我们假设两个 JAR 使用不同的临时文件来存储共享库的副本。但是共享库的两个版本都有调用本地 (C) 函数的本地代码,这些函数具有相同的声明(但这些函数的实现不同)。 JVM/class 加载程序/System.load 是否会将 Java 代码委托给正确的本机代码?或者 JVM 会抱怨名称冲突吗?

如果该方案 失败了,我如何 使用使用本机代码的 class 的多个版本?

如果您尝试在不同的 class 加载程序中加载 相同的 库,您将得到 UnsatisfiedLinkError 和消息 "Native Library: ... already loaded in another class loader"。这可能与 VM 在 class 加载器被垃圾回收时调用库的卸载方法有关 (https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#compiling_loading_and_linking_native_methods)。

但是如果您 - 正如您所说 - "use a different temporary file to store the copy of the shared library" 无论文件内容如何,​​这两个库实际上是不同的库(可能是相同的二进制文件,无关紧要)。所以没有问题。

检查 Open JDK 7 实现,似乎,是的,加载使用本机代码的 Java classes 的多个版本 工作:

正在加载库

关键信息是,System.load 表现如何?该方法的实现将取决于系统,但各种实现的语义应该相同。

  1. System.load 委托包私有方法 Runtime.load0.
  2. Runtime.load0 委托给包私有静态方法 ClassLoader.loadLibrary.
  3. ClassLoader.loadLibrary 委托给私有静态方法 ClassLoader.loadLibrary0.
  4. ClassLoader.loadLibrary0 创建包私有内部对象 class ClassLoader.NativeLibrary 并委托给它的 load 方法。
  5. ClassLoader.NativeLibrary.load是一个本地方法,委托给函数JVM_LoadLibrary.
  6. JVM_LoadLibrary 委托给 os::dll_load.
  7. os::dll_load 取决于系统。
  8. The Linux variant of os::dll_load 委托给 dlopen 系统调用,给出 RTLD_LAZY 选项。
  9. Linux variant of the POSIX dlopen系统调用默认有RTLD_LOCAL行为,所以共享库加载RTLD_LOCAL语义。
  10. RTLD_LOCAL 语义是加载库中的符号 not 可用于后续加载库的(自动)符号解析。也就是说,符号不进入全局命名空间,不同的库可以定义相同的符号而不会产生冲突。共享库 could even have identical content without problems.
  11. 因此,由不同的 class 加载器加载的不同共享库是否定义相同的符号(对于本机方法具有相同的 extern 函数名称)并不重要:JRE 和 JVM一起避免名称冲突。

本机函数查找

这确保了多个版本的共享库不会产生名称冲突。但是 OpenJDK 如何确保 正确的 JNI 代码用于本地方法调用?

  1. procedure followed by the JVM to call a native method 相当冗长,但它全部包含在一个函数 SharedRuntime::generate_native_wrapper 中。然而,最终,它需要知道要调用的 JNI 函数的地址。
  2. 该包装函数使用 methodHandle C++ 对象,根据需要从 methodHandle::critical_native_function()methodHandle::native_function() 获取 JNI 函数的地址。
  3. 通过从NativeLookup::lookup调用methodHandle::set_native_function,JNI函数的地址记录在methodHandle中。
  4. NativeLookup::lookup 间接委托给 NativeLookup::lookup_style
  5. NativeLookup::lookup_style 委托给 Java 包私有静态方法 ClassLoader.findNative.
  6. ClassLoader.findNative 按照库的加载顺序遍历由 ClassLoader.loadLibrary0 设置的 ClassLoader.NativeLibrary 对象的列表 (ClassLoader.nativeLibraries)。对于每个库,它委托 NativeLibrary.find 尝试找到感兴趣的本机方法。虽然这个对象列表不是 public,JNI specification 需要 JVM "maintains a list of loaded native libraries for each class loader",所以所有的实现都必须有类似于这个列表的东西。
  7. NativeLibrary.find 是原生方法。它只是委托给 JVM_FindLibraryEntry.
  8. JVM_FindLibraryEntry 委托给系统依赖方法 os::dll_lookup.
  9. os::dll_lookup 的 Linux 实现委托给 dlsym 系统调用来查找共享库中函数的地址。
  10. 因为每个 class-loader 维护自己的已加载库列表,所以保证为本地方法调用的 JNI 代码将是正确的版本,即使不同的 class loader加载不同版本的共享库。