JNI库文件加载源码解析

计算机软件开发 2024-9-27 20:56:00 6 0 来自 中国
下面我们团结源码详细探讨下JNI调用的库文件是怎样加载的,为啥HelloWorld.so必须被定名成libHelloWorld.so,JNI_OnLoad方法是在什么时候回调的,返回的版本号有啥用?先看下总体的流程图
Java源码解析

System.loadLibrary和System.load方法

System.loadLibrary(String)方法用来加载动态链接库的,String参数是指定动态链接库的模块名的而非真实的文件名的。System还有别的一个load(String)方法,也是用来加载动态链接库的,不外String参数是库文件的绝对路径名,好比上述示例中的System.loadLibrary("HelloWorld") 可以更换成System.load("/home/openjdk/cppTest/HelloWorld.so")。两者的区别在于前者的库文件位置是设置化的,更机动;而后者是代码写死的,实用于测试场景,生产不发起使用。两者的源码如下:
public static void load(String filename) {        Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);    }  public static void loadLibrary(String libname) {        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);}查察两者的表明,load方法要求fileName必须是绝对路径,JVM解析时会去除路径部门和文件后缀得到文件名,使用文件名天生该动态链接库的模块名,好比文件名是HelloWorld,JVM中用JNI_OnLoad_HelloWorld来体现这个模块;loadLibrary方法则直接使用JNI_OnLoad_libname作为模块名。
从类图我们可以知道接下来会调用Runtime类的loadLibrary0方法,在这个方法内里会做两件事:

  • 通过loader.findLibrary(libraryName)找到对应库的全路径
  • 通过doLoad(filename, loader)加载库文件
Runtime.loadLibrary和Runtime.load方法

Runtime的这两个方法的源码如下:
public void load(String filename) {        load0(Reflection.getCallerClass(), filename);    }     synchronized void load0(Class<?> fromClass, String filename) {        SecurityManager security = System.getSecurityManager();        if (security != null) {            security.checkLink(filename);        }        //查抄是否是绝对路径        if (!(new File(filename).isAbsolute())) {            throw new UnsatisfiedLinkError(                "Expecting an absolute path of the library: " + filename);        }        ClassLoader.loadLibrary(fromClass, filename, true);    }  public void loadLibrary(String libname) {        loadLibrary0(Reflection.getCallerClass(), libname);    }     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.loadLibrary方法完成动态链接库文件的加载。
ClassLoader.loadLibrary方法

static void loadLibrary(Class<?> fromClass, String name,                            boolean isAbsolute) {      //fromClass是一开始调用System的类,getClassLoader一样寻常返回AppClassLoader实例,      //只有通过启动类加载器加载的类如String,getClassLoader会返回null       ClassLoader loader =            (fromClass == null) ? null : fromClass.getClassLoader();        //usr_paths和sys_paths是ClassLoader的两个静态属性,假如为空则初始化        if (sys_paths == null) {            usr_paths = initializePath("java.library.path");            sys_paths = initializePath("sun.boot.library.path");        }        //假如是绝对路径        if (isAbsolute) {            if (loadLibrary0(fromClass, new File(name))) {                return;            }            throw new UnsatisfiedLinkError("Can't load library: " + name);        }        //假如loader不为空,尝试通过findLibrary查找指定模块的绝对路径        if (loader != null) {            //ClassLoader的实现默认返回空,只有ExtClassLoader改写了该方法            String libfilename = loader.findLibrary(name);            if (libfilename != null) {                File libfile = new File(libfilename);                //校验是否是绝对路径,假如不为null则必须是绝对路径                if (!libfile.isAbsolute()) {                    throw new UnsatisfiedLinkError(    "ClassLoader.findLibrary failed to return an absolute path: " + libfilename);                }                if (loadLibrary0(fromClass, libfile)) {                    return;                }                throw new UnsatisfiedLinkError("Can't load " + libfilename);            }        }        //无论loader是否为空        //遍历sys_paths下的子路径,查找是否存在目的文件        for (int i = 0 ; i < sys_paths.length ; i++) {            //获取映射后的子路径下的文件名,mapLibraryName是当地方法            File libfile = new File(sys_paths, System.mapLibraryName(name));            //尝试加载目的文件,假如存在则返回            if (loadLibrary0(fromClass, libfile)) {                return;            }            //mapAlternativeName默认返回null            libfile = ClassLoaderHelper.mapAlternativeName(libfile);            if (libfile != null && loadLibrary0(fromClass, libfile)) {                return;            }        }        //假如loader不为空,通常会走到此逻辑        if (loader != null) {            //遍历usr_paths下的全部子路径            for (int i = 0 ; i < usr_paths.length ; i++) {                //获取子路径下映射过的文件名                File libfile = new File(usr_paths,                                        System.mapLibraryName(name));                if (loadLibrary0(fromClass, libfile)) {                    return;                }                libfile = ClassLoaderHelper.mapAlternativeName(libfile);                if (libfile != null && loadLibrary0(fromClass, libfile)) {                    return;                }            }        }        //没有找到,加载失败,抛出非常        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");    } protected String findLibrary(String libname) {        return null;    }findLibrary被改写的子类如下:
1.png System.mapLibraryName是一个当地方法,体现将库名映射成特定平台下的库文件名,界说如下:
ClassLoader.loadLibrary0方法

private static boolean loadLibrary0(Class<?> fromClass, final File file) {        //查抄是否是内置的动态链接库,findBuiltinLib是当地方法        String name = findBuiltinLib(file.getName());        boolean isBuiltin = (name != null);        //假如不是        if (!isBuiltin) {            boolean exists = AccessController.doPrivileged(                new PrivilegedAction<Object>() {                    public Object run() {                        //查抄文件是否存在                        return file.exists() ? Boolean.TRUE : null;                    }})                != null;            //文件不存在返回false            if (!exists) {                return false;            }            try {            //获取简洁情势的路径                name = file.getCanonicalPath();            } catch (IOException e) {                return false;            }        }        //获取调用类的类加载器已加载的库文件缓存,假如为空则使用体系类加载对应的缓存        ClassLoader loader =            (fromClass == null) ? null : fromClass.getClassLoader();        //nativeLibraries是私有实例属性,体现该ClassLoader已经加载过的共享库缓存        Vector<NativeLibrary> libs =            loader != null ? loader.nativeLibraries : systemNativeLibraries;        synchronized (libs) {            int size = libs.size();            //遍历缓存            for (int i = 0; i < size; i++) {                NativeLibrary lib = libs.elementAt(i);                //假如找到同名的阐明已经加载过了,则返回true                if (name.equals(lib.name)) {                    return true;                }            }            //loadedLibraryNames是ClassLoader的静态属性,Vector<String>范例,体现全局的全部Class            Loader实例已加载的共享库的文件名的缓存            //两层的synchronized控制包管极端并发环境下只有一个线程加载共享库            synchronized (loadedLibraryNames) {                //假如存在阐明该库文件已经被其他的某个ClassLoader实例加载过了                if (loadedLibraryNames.contains(name)) {                    throw new UnsatisfiedLinkError                        ("Native Library " +                         name +                         " already loaded in another classloader");                }                               //nativeLibraryContext是ClassLoader的静态属性,Stack<NativeLibrary>范例的,体现正在加载大概卸载的共享库缓存                int n = nativeLibraryContext.size();                for (int i = 0; i < n; i++) {                    NativeLibrary lib = nativeLibraryContext.elementAt(i);                    if (name.equals(lib.name)) {                        //假如存在同名的且是同一个ClassLoader实例则返回true,差别的则返回false                        if (loader == lib.fromClass.getClassLoader()) {                            return true;                        } else {                            throw new UnsatisfiedLinkError                                ("Native Library " +                                 name +                                 " is being loaded in another classloader");                        }                    }                }                NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);                //入栈                nativeLibraryContext.push(lib);                try {                   //实行现实的加载任务,load是ClassLoader的静态内部类NativeLibrary的当地方法                    lib.load(name, isBuiltin);                } finally {                    //出栈                    nativeLibraryContext.pop();                }                //假如加载乐成                if (lib.loaded) {                    //将该实例加入到缓存中                    loadedLibraryNames.addElement(name);                    libs.addElement(lib);                    return true;                }                return false;            }        }    }此中NativeLibrary是ClassLoader的静态内部类,包级访问,该类就是用来体现一个已经加载过的库文件,每个ClassLoader实例加载的库文件对应NativeLibrary实例都生存在ClassLoader的nativeLibraries属性中,全部ClassLoader实例加载过NativeLibrary实例都生存在ClassLoader的全局systemNativeLibraries属性中,每个库文件都有依赖的JDK版本,NativeLibrary的属性jniVersion就是用来纪录该库文件依赖的JDK版本,JVM加载库文件时会校验是否支持库文件要求的JDK版本,属性loaded体现该库文件是否已加载。
上述源码解析表明同一个共享库文件只能被一个ClassLoader实例加载,由于对JVM而言同名的库文件加载一次就行了,包罗当地方法的类可以被差别ClassLoader实例加载天生差别的Klass,但是他们的当地方法实行时可以调用相同的当地方法实现。那么差别名的库文件假如包罗同名的方法实现会怎样呢?
可以把上述示例中的HelloWorld.cpp的打印调解下,重新编译天生一个新的so文件,然后在HelloWorld.java中同时加载两个so文件,新的so文件第一个加载,效果如下:
3.png 实行多次,从效果看使用的是第一个加载的共享库的方法实现,即多个共享库假如存在同名方法实现则JVM只绑定第一个加载的对应的方法实现,当地方法的这种特别性是JVM调用操纵体系加载动态链接库的实现决定的,也跟C语言不支持方法同名有关系,必要在生产应用中重点注意,为了制止方法同名所以天生头文件时方法名会包罗全限定类名,但是这无法办理同一个类差别版本的同名方法的问题。
C++源码解析

上节Java源码分析中发现库文件加载涉及三个当地方法,String mapLibraryName(String libname),String findBuiltinLib(String name)和load(String name, boolean isBuiltin)方法,第一个方法的实现参考OpenJDK jdk/src/share/native/java/lang/System.c,第二个参考同一目次下ClassLoader.c。
mapLibraryName

JNIEXPORT jstring JNICALLJava_java_lang_System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname){    int len;    //前缀的长度    int prefix_len = (int) strlen(JNI_LIB_PREFIX);    //后缀的长度    int suffix_len = (int) strlen(JNI_LIB_SUFFIX);     jchar chars[256];    //非空校验    if (libname == NULL) {        JNU_ThrowNullPointerException(env, 0);        return NULL;    }    //长度校验    len = (*env)->GetStringLength(env, libname);    if (len > 240) {        JNU_ThrowIllegalArgumentException(env, "name too long");        return NULL;    }    //将前缀复制到数组中    cpchars(chars, JNI_LIB_PREFIX, prefix_len);    //将libname复制到数组中前缀的反面    (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);    len += prefix_len;    //将后缀复制到数组中libname的反面    cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);    len += suffix_len;    //拼成一个新的字符串    return (*env)->NewString(env, chars, len);}Linux下前缀后缀的界说在hotspot/src/os/linux/vm/jvm_linux.h中,如下:

这就表明了为啥把HelloWorld.so 改成libHelloWorld.so就可以正常加载该库文件了。
findBuiltinLib

JNIEXPORT jstring JNICALLJava_java_lang_ClassLoader_findBuiltinLib  (JNIEnv *env, jclass cls, jstring name){    const char *cname;    char *libName;    int prefixLen = (int) strlen(JNI_LIB_PREFIX);    int suffixLen = (int) strlen(JNI_LIB_SUFFIX);    int len;    jstring lib;    void *ret;    const char *onLoadSymbols[] = JNI_ONLOAD_SYMBOLS;    //非空校验    if (name == NULL) {        JNU_ThrowInternalError(env, "NULL filename for native library");        return NULL;    }    //初始化procHandle,现实是一个dlopen函数的额指针    procHandle = getProcessHandle();    //将name中的字符串拷贝到cname中    cname = JNU_GetStringPlatformChars(env, name, 0);    if (cname == NULL) {        return NULL;    }    //校验cname的长度是否大于前缀长度加上后缀长度    len = strlen(cname);    if (len <= (prefixLen+suffixLen)) {        JNU_ReleaseStringPlatformChars(env, name, cname);        return NULL;    }    //libName初始化    libName = malloc(len + 1); //+1 for null if prefix+suffix == 0    if (libName == NULL) {        JNU_ReleaseStringPlatformChars(env, name, cname);        JNU_ThrowOutOfMemoryError(env, NULL);        return NULL;    }    //跳过前缀将cname复制到libName中    if (len > prefixLen) {        strcpy(libName, cname+prefixLen);    }    //开释cname对应的内存    JNU_ReleaseStringPlatformChars(env, name, cname);    //将后缀起始字符置为\0,标记字符串竣事,即相当于去掉了后缀    libName[strlen(libName)-suffixLen] = '\0';     //查找该libname是否存在    ret = findJniFunction(env, procHandle, libName, JNI_TRUE);    if (ret != NULL) {        //假如存在返回,用libname构造一个java String并返回        lib = JNU_NewStringPlatform(env, libName);        //开释libName的内存        free(libName);        return lib;    }    //假如不存在,开释libName的内存,返回NULL    free(libName);    return NULL;} static void *findJniFunction(JNIEnv *env, void *handle,                                    const char *cname, jboolean isLoad) {    //字符串指针数组,现实是{"JNI_OnLoad"}                                    const char *onLoadSymbols[] = JNI_ONLOAD_SYMBOLS;    //现实是{"JNI_OnUnload"}    const char *onUnloadSymbols[] = JNI_ONUNLOAD_SYMBOLS;    const char **syms;    int symsLen;    void *entryName = NULL;    char *jniFunctionName;    int i;    int len;     // 根据isLoad判断 JNI_On(Un)Load<_libname>    if (isLoad) {        syms = onLoadSymbols;        symsLen = sizeof(onLoadSymbols) / sizeof(char *);    } else {        syms = onUnloadSymbols;        symsLen = sizeof(onUnloadSymbols) / sizeof(char *);    }    for (i = 0; i < symsLen; i++) {        //查抄拼起来的JNI_On(Un)Load<_libname>的长度是否大于最大值FILENAME_MAX        if ((len = (cname != NULL ? strlen(cname) : 0) + strlen(syms) + 2) >            FILENAME_MAX) {            goto done;        }        //给jniFunctionName分配内存        jniFunctionName = malloc(len);        if (jniFunctionName == NULL) {            JNU_ThrowOutOfMemoryError(env, NULL);            goto done;        }        //拼成JNI_On(Un)Load<_libname>,拼成的字符串作为底层ddl查找的参数,libname就是要查找的库文件,JNI_OnLoad就是在库文件中查找的目的函数名        buildJniFunctionName(syms, cname, jniFunctionName);        //查找该方法是否已加载        entryName = JVM_FindLibraryEntry(handle, jniFunctionName);        //开释内存        free(jniFunctionName);        //假如不为空则停止循环,返回entryName        if(entryName) {            break;        }    }  done:    return entryName;} void buildJniFunctionName(const char *sym, const char *cname,                          char *jniEntryName) {    //将sym复制到数组jniEntryName中                          strcpy(jniEntryName, sym);    if (cname != NULL) {        //字符勾通接        strcat(jniEntryName, "_");        strcat(jniEntryName, cname);    }}load

JNIEXPORT void JNICALLJava_java_lang_ClassLoader_00024NativeLibrary_load  (JNIEnv *env, jobject this, jstring name, jboolean isBuiltin){    const char *cname;    jint jniVersion;    jthrowable cause;    void * handle;    //jniVersionID等初始化    if (!initIDs(env))        return;        //将java String对象的字符串复制到cname 字符数组中    cname = JNU_GetStringPlatformChars(env, name, 0);    if (cname == 0)        return;    //假如未加载则加载该库文件    handle = isBuiltin ? procHandle : JVM_LoadLibrary(cname);    //假如加载完成    if (handle) {        JNI_OnLoad_t JNI_OnLoad;        //获取该库文件中的JNI_OnLoad函数        JNI_OnLoad = (JNI_OnLoad_t)findJniFunction(env, handle,                                               isBuiltin ? cname : NULL,                                               JNI_TRUE);        //假如库文件包罗了JNI_OnLoad函数                                             if (JNI_OnLoad) {            JavaVM *jvm;            //通过JNIEnv获取对应的JavaVM            (*env)->GetJavaVM(env, &jvm);            //获取库文件要求的jniVersion            jniVersion = (*JNI_OnLoad)(jvm, NULL);        } else {            jniVersion = 0x00010001;        }         //假如出现非常        cause = (*env)->ExceptionOccurred(env);        if (cause) {            (*env)->ExceptionClear(env);            (*env)->Throw(env, cause);            if (!isBuiltin) {                JVM_UnloadLibrary(handle);            }            goto done;        }         //假如库文件要求的jniVersion不支持则抛出非常        if (!JVM_IsSupportedJNIVersion(jniVersion) ||            (isBuiltin && jniVersion < JNI_VERSION_1_8)) {            char msg[256];            jio_snprintf(msg, sizeof(msg),                         "unsupported JNI version 0x%08X required by %s",                         jniVersion, cname);            JNU_ThrowByName(env, "java/lang/UnsatisfiedLinkError", msg);            if (!isBuiltin) {                JVM_UnloadLibrary(handle);            }            goto done;        }        //假如库文件要求的jniVersion支持则设置NativeLibrary实例的jniVersion属性        (*env)->SetIntField(env, this, jniVersionID, jniVersion);    } else {        cause = (*env)->ExceptionOccurred(env);        if (cause) {            (*env)->ExceptionClear(env);            (*env)->SetLongField(env, this, handleID, (jlong)0);            (*env)->Throw(env, cause);        }        goto done;    }    //假如加载乐成,则设置NativeLibrary实例的handle属性和loaded属性    (*env)->SetLongField(env, this, handleID, ptr_to_jlong(handle));    (*env)->SetBooleanField(env, this, loadedID, JNI_TRUE);  done:    //开释cname对应的内存    JNU_ReleaseStringPlatformChars(env, name, cname);} static jboolean initIDs(JNIEnv *env){    //handleID,jniVersionID,loadedID都是静态全局变量,体现NativeLibrary类的handle,jniVersion,loaded三个属性的属性ID    //可通过属性ID设置实例的属性    //当第一次加载库文件的时候会根据NativeLibrary类初始化这些静态属性    if (handleID == 0) {        jclass this =            (*env)->FindClass(env, "java/lang/ClassLoader$NativeLibrary");        if (this == 0)            return JNI_FALSE;        handleID = (*env)->GetFieldID(env, this, "handle", "J");        if (handleID == 0)            return JNI_FALSE;        jniVersionID = (*env)->GetFieldID(env, this, "jniVersion", "I");        if (jniVersionID == 0)            return JNI_FALSE;        loadedID = (*env)->GetFieldID(env, this, "loaded", "Z");        if (loadedID == 0)             return JNI_FALSE;        procHandle = getProcessHandle();    }    return JNI_TRUE;}上述分析表明了实行System.loadLibrary方法的时候详细是在哪回调库文件中界说的JNI_OnLoad方法以及JNI_OnLoad方法返回的JDK版本的用途。
JVM_FindLibraryEntry和JVM_LoadLibrary

JVM_LoadLibrary的方法界说在jvm.h中,实现在hotspot/src/share/vm/prims/jvm.cpp中,如下图: 5.png JVM_FindLibraryEntry的实现如下:

差别操纵体系实现不一样,这里涉及较多的操纵体系相干的知识,这里不做讨论。
总结

将上述Java源码和C++源码的分析串联起来,结论就是,当调用System.loadLibrary方法加载库文件的时候,会起首尝试从已加载的库文件中查找是否存在目的库文件的JNI_OnLoad方法,假如没有,则大概是库文件不存在,库文件未加载大概库文件已加载但是没有界说该方法三种缘故起因。所以起首判断该库文件是否存在,假如不存在则返回加载失败。假如存在则在已加载的NativeLibrary实例聚会集查找是否存在库文件名同等的NativeLibrary实例,假如存在则返回该实例体现已加载完成。假如不存在则阐明该文件未加载,然后调用JVM_LoadLibrary加载该文件,加载完成重新查找该文件中的JNI_OnLoad方法,假如界说了则调用该方法获取该库文件要求的JDK版本,假如没有界说则默以为JDK1.1,判断当前JVM是否支持该版本,假如支持则设置NativeLibrary实例的相干属性,并将其添加到已加载的NativeLibrary实例聚会集,团体流程图如下:团结上述分析也可得出System.loadLibrary方法现实只是完成了库文件加载而已,假如未实行RegisterNatives体现绑定当地方法和Java方法,则两者之间的绑定只有到该方法被初次调用时才气完成,因此对于必要频仍调用的方法发起使用RegisterNatives方法在库文件加载时即完成绑定,制止方法调用时再去查找时的性能消耗。
JDK库文件加载

JDK中的尺度类如java.lang.Object中并没有调用System.loadLibrary方法加载库文件,那么JDK依赖的库文件是谁加载的?什么时候加载的?可以查找os::dll_load方法的调用链,如下图:此中os::native_java_library() 方法就是答案,该方法在JVM初始化的时候调用的,该方法加载ddl的实现如下:

9.png
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 18:28, Processed in 0.181409 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表