美文网首页Android知识
OpenJDK源码学习-加载本地库

OpenJDK源码学习-加载本地库

作者: 骆驼骑士 | 来源:发表于2017-04-07 20:43 被阅读701次

    作者:lds(lds2012@gmail.com)

    日期:2017-04-07

    前言

    本文主要研究OpenJDK源码中涉及到加载native本地库的部分。主要目的是为了了解本地库是如何被加载到虚拟机,如果执行其中的本地方法,以及JNI的 JNI_OnLoadJNI_OnUnLoad是如何被调用的 。

    1.载入本地库

    使用JNI的第一步,往往是在Java代码里面加载本地库的so文件,例如:

    public class Test {
      static {
        System.loadLibrary("my_native_library_name");
      }
    }
    

    那么我们从这个方法作为入口来研究JDK的代码。

    2. 寻找本地库文件

    System.java

    源码在 OpenJdk/jdk/src/share/classes/java/lang/System.java

        public static void loadLibrary(String libname) {
            Runtime.getRuntime().loadLibrary0(getCallerClass(), libname);
        }
    

    Runtime.java

    源码在 OpenJdk/jdk/src/share/classes/java/lang/Runtime.java

    然后来看 java.lang.Runtime 类时如何来 loadLibrary 的:

        synchronized void loadLibrary0(Class fromClass, String libname) {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkLink(libname);
            }
            if (libname.indexOf((int)File.separatorChar) != -1) {
                throw new UnsatisfiedLinkError(
        "Directory separator should not appear in library name: " + libname);
            }
            ClassLoader.loadLibrary(fromClass, libname, false);
        }
    

    它首先做了一些安全性检查,然后使用 ClassLoader 来载入本地库的。

    ClassLoader.java

    源码在 OpenJdk/jdk/src/share/classes/java/lang/ClassLoader.java

    接下来看 ClassLoader 是如何实现的具体的加载工作的:

    首先根据 libname 参数找到本地库的文件路径,并访问该so库文件来载入,

    在这里会在几个地方去找so库文件:

    1. Class Loader找到的绝对路径(class path)
    2. java.library.path 定义的目录下(windows下的PATH,linux下的LD_LIBRARY_PATH)
    3. sun.boot.library.path 定义的目录下
    usr_paths = initializePath("java.library.path");
    sys_paths = initializePath("sun.boot.library.path");
    
    // 首先从class loader的目录读取
    String libfilename = loader.findLibrary(name);
    // ...
    File libfile = new File(libfilename);
    // ...
    if (loadLibrary0(fromClass, libfile)) {
        return;
    }
    
    // 然后尝试从sys_paths目录下读取so文件
    for (int i = 0 ; i < sys_paths.length ; i++) {
        File libfile = new File(sys_paths[i], System.mapLibraryName(name));
        if (loadLibrary0(fromClass, libfile)) {
            return;
        }
    }
    
    // 最后尝试从usr_paths目录下读取so文件
    for (int i = 0 ; i < usr_paths.length ; i++) {
        File libfile = new File(usr_paths[i], System.mapLibraryName(name));
        if (loadLibrary0(fromClass, libfile)) {
            return;
        }
    }
    

    其中可以看到,对于传入给 System.loadLibrary(String libname) 的参数 libname 是通过调用 System.mapLibraryName 方法来将其映射为库文件的文件名。

    这个方法是一个native方法,不同系统有不同的实现,具体的区别主要在于前缀和扩展名的不同,例如在 linux 平台下前缀和扩展名分为定义为:

    #define JNI_LIB_PREFIX "lib"
    #define JNI_LIB_SUFFIX ".so"
    

    3. 维护本地库列表

    对于找到so库文件以后,具体的加载工作是由 loadLibrary0 方法来完成的,

    首先如果有 ClassLoader 则将本地库加载到该 ClassLoader 的本地库列表中,如果没有则加载到系统本地库列表中。

    ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
    Vector<NativeLibrary> libs =
                loader != null ? loader.nativeLibraries : systemNativeLibraries;
    

    然后遍历已经加载的本地库列表,如果发现这个本地库已经被system或这个classLoader加载过了,则不再执行加载工作,直接返回true。这里也防止了我们重复的去调用 System.loadLibrary 去加载同一个库。

    这里需要注意的是一个本地库只能被同一个 ClassLoader (或线程)加载,一旦被某个 ClassLoader (或线程)加载过了,再使用另一个 ClassLoader (或线程)去加载它,则会抛出异常。

    然后的本地库都会被封装成 NativeLibrary 对象,并存入 ClassLoader 的静态Stack里面。然后调用它的 load 方法来完成加载功能。

    这里需要先了解一下,本地库被谁加载,加载以后存在哪里:

    首先系统类去维护一个本地库列表,其中保存了由系统加载的本地库名称。

    // Native libraries belonging to system classes.
    private static Vector<NativeLibrary> systemNativeLibraries = new Vector<>();
    

    然后每个 ClassLoader 实例都必须去维护一个列表,其中保存了所有由它加载过的本地库名称。

    // Native libraries associated with the class loader.
    private Vector<NativeLibrary> nativeLibraries = new Vector<>();
    

    最有所有的被加载过的本地库名称列表,以静态变量的形式保存起来。

    // All native library names we've loaded.
    private static Vector<String> loadedLibraryNames = new Vector<>();
    

    然后所有的本地库在加载后,都被以 NativeLibrary 类型保存在 ClassLoader 的静态Stack里。

    NativeLibrary

    源码在 OpenJdk/jdk/src/share/classes/java/lang/ClassLoader.java

    NativeLibrary是ClassLoader的静态内部类,用于封装已经加载过的本地库信息。每个NativeLibrary对象都需要有一个JNI的版本号。这个版本号是虚拟机在载入本地库的时候获取并设置的。

    它有主要的三个方法,并且它们都是native方法,依次是:

    native void load(String name);
    
    native long find(String name);
    
    native void unload();
    

    load 方法用于加载本地库。

    find 方法用于找到本地库的指针地址。

    unload 方法用于卸载本地库。

    另外在其 finalize 方法里,将其从 ClassLoader 中保存的已加载本地库列表中移除。

    4. 加载和卸载本地库

    ClassLoader.c

    源码在:OpenJDK/jdk/src/share/native/java/lang/ClassLoader.c

    在此主要关注java层的NativeLibrary类其中的三个native方法,来了解具体是如何加载和卸载本地库的。

    NativeLibrary_load

    首先来看本地代码是如何加载一个本地库的。

    JNIEXPORT void JNICALL
    Java_java_lang_ClassLoader_00024NativeLibrary_load
      (JNIEnv *env, jobject this, jstring name)
    

    注意:这里的 _00024 表示的是 $ 符号,用来在java中表示内部类。

    这里需要说明的是最后一个参数 name ,它是在构建一个 NativeLibrary 对象时传进来的,是本地库文件的完整路径,其是调用 Java 中的 File.getCanonicalPath() 方法来获取的。

    Step 1: 先加载本地库文件

    其中最关键的在于根据传入的这个 name (会将jstring类型转换成char*类型),来加载本地库:

    handle = JVM_LoadLibrary(cname);
    

    Step 2: 再执行JNI_OnLoad函数

    在其加载成功后,会去寻找 JNI_OnLoad 函数,并执行它, JNI_OnLoad 函数返回的其使用的JNI版本号的值,如果没有找到该方法,则默认使用 JNI 1_1 作为版本号。

    如果返回的是一个不支持的版本号,则会抛出 UnsatisfiedLinkError 异常。

    其中 JVM_LoadLibrary 函数定义为:

    OpenJdk/jdk/src/share/javavm/export 目录下的 jvm.h 文件中:

    JNIEXPORT void * JNICALL JVM_LoadLibrary(const char *name);
    

    具体的实现由虚拟机在实现,例如 hotspot 的实现在

    OpenJdk/hotspot/src/share/vm/prims 目录下的 jvm.cpp 文件:

    JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
      //%note jvm_ct
      JVMWrapper2("JVM_LoadLibrary (%s)", name);
      char ebuf[1024];
      void *load_result;
      {
        ThreadToNativeFromVM ttnfvm(thread);
        load_result = os::dll_load(name, ebuf, sizeof ebuf);
      }
      if (load_result == NULL) {
        char msg[1024];
        jio_snprintf(msg, sizeof msg, "%s: %s", name, ebuf);
        // Since 'ebuf' may contain a string encoded using
        // platform encoding scheme, we need to pass
        // Exceptions::unsafe_to_utf8 to the new_exception method
        // as the last argument. See bug 6367357.
        Handle h_exception =
          Exceptions::new_exception(thread,
                                    vmSymbols::java_lang_UnsatisfiedLinkError(),
                                    msg, Exceptions::unsafe_to_utf8);
    
        THROW_HANDLE_0(h_exception);
      }
      return load_result;
    JVM_END
    

    其中能看到重要的在于 os::dll_load 函数,它是根据系统不同而由不同的实现的。

    linux实现

    例如在 linux 系统下的实现在 openjdk/hotspot/src/os/linux/vm/os_linux.cpp 文件中。

    它其中主要做了两件事情,一个是使用 linux 的 dlopen 来打开这个so本地库文件,再则检查了这个so本地库文件是否和当前运行虚拟机的CPU架构是否相同。

    dlopen函数定义在 dlfcn.h,原型为:

    void * dlopen( const char * pathname, int mode); 
    

    其中第二个参数使用的是 RTLD_LAZY: 异常绑定。

    windows实现

    windows的实现是使用 LoadLibrary 函数来加载 dll 本地库。

    NativeLibrary_unload

    Step1: 先执行JNI_OnUnLoad方法

    虚拟机在卸载本地库文件之前,会先回调本地库文件中的 JNI_OnUnLoad 函数,可以在该函数中执行一些清理工作,例如清理全局变量等。

    Step2: 再卸载本地库文件

    JVM_UnloadLibrary 和 ``JVM_loadLibrary` 函数一样,具体根据平台不同而实现:

    在linux平台上,使用 dlopen 函数来 load so文件, 使用 dlclose 函数来 unload.

    在windows平台上,使用 LoadLibrary 函数来load dll文件,来 FreeLibrary 函数来 unload.

    NativeLibrary_find

    寻找本地库里的某个方法或全局变量的内存地址。

    在不同平台上的实现不一样:

    在linux平台上,使用dlsym 函数来获取某个方法的内存地址。

    在windows平台上,使用 GetProcAddress 函数来获取某个方法的内存地址。

    ​注意:在 NativeLibrary_loadNativeLibrary_unload 两个函数内,不是调用了so库里面的 JNI_OnLoadJNI_OnUnLoad 函数嘛,其就是使用 NativeLibrary_find 函数来找到这两个函数地址,并执行它们了。

    handle = jlong_to_ptr((*env)->GetLongField(env, this, handleID));
    JNI_OnUnload = (JNI_OnUnload_t )
                JVM_FindLibraryEntry(handle, onUnloadSymbols[i]);
    
    if (JNI_OnUnload) {
        JavaVM *jvm;
        (*env)->GetJavaVM(env, &jvm);
        (*JNI_OnUnload)(jvm, NULL);
    }
    

    这里面有一个比较重要的变量就是 handleID ,这个handleID是从哪里来,存在哪里都比较关键。

    首先我们来看这个handleID来至哪里,它其实是 JVM_LoadLibrary 返回的值,即 dlopen 返回的值,这个比较简单,它是在打开本地库时返回的句柄,然后这个句柄并没有保存在native层,而是将其保存在了Java层。

    在调用 NativeLibrary_load 函数里,将这个 handleID 保存到了这个 NativeLibrary Java对象的 long handle成员域里。每次需要使用 handleID 的时候都从这个Java对象里面的成员域去取。

    5. 加载流程小结

    从整个加载本地库的流程来看,基本上还是调用和平台有关的函数来完成的,并在加载和卸载的时候分别调用了两个生命周期回调函数 JNI_OnLoadJNI_OnUnLoad

    以linux平台为例,简单总结一下整个so库的加载流程:

    1. 首先 System.loadLibrary() 被调用,开始整个加载过程。
    2. 其中调用 ClassLoader 对象来完成主要工作,将每个本地库封装成 NativeLibrary 对象,并以静态变量存到已经加载过的栈中。
    3. 执行NativeLibrary 类的 load native方法,来交给native层去指向具体的加载工作。
    4. native层 ClassLoader.c 中的 Java_java_lang_ClassLoader_00024NativeLibrary_load 函数被调用。
    5. 在native load函数中首先使用 dlopen 来加载so本地库文件,并将返回的handle保存到 NativeLibrary对象中。
    6. 接着查找已经加载的so本地库中的 JNI_OnLoad 函数,并执行它。
    7. 整个so本地库的加载流程完毕。

    只有在 NativeLibrary 对象被GC回收的时候,其 finalize 方法被调用了,对应加载的本地库才会被 unload 。这种情况一般来说并不会发生,因为 NativeLibrary 对象是以静态变量的形式被保存的,而静态变量是 GC roots,一般来说都不会被回收掉的。

    TODO: 那请问 JNI_OnUnLoad 函数什么情况下会被调用?虚拟机关闭的时候?一个本地库被load后,是否能手动的unload?什么情况下才可能被unload?

    结语

    参考资料:

    • OpenJdk/jdk/src/share/classes/java/lang/System.java
    • OpenJdk/jdk/src/share/classes/java/lang/Runtime.java
    • OpenJdk/jdk/src/share/classes/java/lang/ClassLoader.java
    • OpenJDK/jdk/src/share/native/java/lang/ClassLoader.c

    相关文章

      网友评论

        本文标题:OpenJDK源码学习-加载本地库

        本文链接:https://www.haomeiwen.com/subject/wxckattx.html