加载使用本机代码的 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
表现如何?该方法的实现将取决于系统,但各种实现的语义应该相同。
System.load
委托包私有方法 Runtime.load0
.
Runtime.load0
委托给包私有静态方法 ClassLoader.loadLibrary
.
ClassLoader.loadLibrary
委托给私有静态方法 ClassLoader.loadLibrary0
.
ClassLoader.loadLibrary0
创建包私有内部对象 class ClassLoader.NativeLibrary
并委托给它的 load
方法。
ClassLoader.NativeLibrary.load
是一个本地方法,委托给函数JVM_LoadLibrary
.
JVM_LoadLibrary
委托给 os::dll_load
.
os::dll_load
取决于系统。
- The Linux variant of
os::dll_load
委托给 dlopen
系统调用,给出 RTLD_LAZY
选项。
- Linux variant of the POSIX
dlopen
系统调用默认有RTLD_LOCAL
行为,所以共享库加载RTLD_LOCAL
语义。
RTLD_LOCAL
语义是加载库中的符号 not 可用于后续加载库的(自动)符号解析。也就是说,符号不进入全局命名空间,不同的库可以定义相同的符号而不会产生冲突。共享库 could even have identical content without problems.
- 因此,由不同的 class 加载器加载的不同共享库是否定义相同的符号(对于本机方法具有相同的
extern
函数名称)并不重要:JRE 和 JVM一起避免名称冲突。
本机函数查找
这确保了多个版本的共享库不会产生名称冲突。但是 OpenJDK 如何确保 正确的 JNI 代码用于本地方法调用?
- procedure followed by the JVM to call a native method 相当冗长,但它全部包含在一个函数
SharedRuntime::generate_native_wrapper
中。然而,最终,它需要知道要调用的 JNI 函数的地址。
- 该包装函数使用
methodHandle
C++ 对象,根据需要从 methodHandle::critical_native_function()
或 methodHandle::native_function()
获取 JNI 函数的地址。
- 通过从
NativeLookup::lookup
调用methodHandle::set_native_function
,JNI函数的地址记录在methodHandle
中。
NativeLookup::lookup
间接委托给 NativeLookup::lookup_style
NativeLookup::lookup_style
委托给 Java 包私有静态方法 ClassLoader.findNative
.
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",所以所有的实现都必须有类似于这个列表的东西。
NativeLibrary.find
是原生方法。它只是委托给 JVM_FindLibraryEntry
.
JVM_FindLibraryEntry
委托给系统依赖方法 os::dll_lookup
.
os::dll_lookup
的 Linux 实现委托给 dlsym
系统调用来查找共享库中函数的地址。
- 因为每个 class-loader 维护自己的已加载库列表,所以保证为本地方法调用的 JNI 代码将是正确的版本,即使不同的 class loader加载不同版本的共享库。
如果您想加载一个 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
表现如何?该方法的实现将取决于系统,但各种实现的语义应该相同。
System.load
委托包私有方法Runtime.load0
.Runtime.load0
委托给包私有静态方法ClassLoader.loadLibrary
.ClassLoader.loadLibrary
委托给私有静态方法ClassLoader.loadLibrary0
.ClassLoader.loadLibrary0
创建包私有内部对象 classClassLoader.NativeLibrary
并委托给它的load
方法。ClassLoader.NativeLibrary.load
是一个本地方法,委托给函数JVM_LoadLibrary
.JVM_LoadLibrary
委托给os::dll_load
.os::dll_load
取决于系统。- The Linux variant of
os::dll_load
委托给dlopen
系统调用,给出RTLD_LAZY
选项。 - Linux variant of the POSIX
dlopen
系统调用默认有RTLD_LOCAL
行为,所以共享库加载RTLD_LOCAL
语义。 RTLD_LOCAL
语义是加载库中的符号 not 可用于后续加载库的(自动)符号解析。也就是说,符号不进入全局命名空间,不同的库可以定义相同的符号而不会产生冲突。共享库 could even have identical content without problems.- 因此,由不同的 class 加载器加载的不同共享库是否定义相同的符号(对于本机方法具有相同的
extern
函数名称)并不重要:JRE 和 JVM一起避免名称冲突。
本机函数查找
这确保了多个版本的共享库不会产生名称冲突。但是 OpenJDK 如何确保 正确的 JNI 代码用于本地方法调用?
- procedure followed by the JVM to call a native method 相当冗长,但它全部包含在一个函数
SharedRuntime::generate_native_wrapper
中。然而,最终,它需要知道要调用的 JNI 函数的地址。 - 该包装函数使用
methodHandle
C++ 对象,根据需要从methodHandle::critical_native_function()
或methodHandle::native_function()
获取 JNI 函数的地址。 - 通过从
NativeLookup::lookup
调用methodHandle::set_native_function
,JNI函数的地址记录在methodHandle
中。 NativeLookup::lookup
间接委托给NativeLookup::lookup_style
NativeLookup::lookup_style
委托给 Java 包私有静态方法ClassLoader.findNative
.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",所以所有的实现都必须有类似于这个列表的东西。NativeLibrary.find
是原生方法。它只是委托给JVM_FindLibraryEntry
.JVM_FindLibraryEntry
委托给系统依赖方法os::dll_lookup
.os::dll_lookup
的 Linux 实现委托给dlsym
系统调用来查找共享库中函数的地址。- 因为每个 class-loader 维护自己的已加载库列表,所以保证为本地方法调用的 JNI 代码将是正确的版本,即使不同的 class loader加载不同版本的共享库。