您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

只需一篇就让你详细了解 Java 中 so 文件的加载原理

时间:2023-02-10 12:32:27  来源:今日头条  作者:谷湫慕

/ 前言 /

 

无论是 Android 开发者还是 JAVA 工程师应该都有使用过 JNI 开发,但对于 JVM 如何加载 so、Android 系统如何加载 so,可能鲜有时间了解。

 

本文通过代码、流程解释,带大家快速了解其加载原理,扫清困惑。

 

/ System#load() + loadLibrary() /

 

load()

 

System 提供的 load() 用于指定 so 的完整的路径名且带文件后缀并加载,等同于调用 Runtime 类提供的 load()。

 

If the filename argument, when stripped of any platform-specific library prefix, path, and file extension, indicates a library whose name is, for example, L, and a native library called L is statically linked with the VM, then the JNI_OnLoad_L function exported by the library is invoked rather than attempting to load a dynamic library.

 

Eg.

 

System.load("/sdcard/path/libA.so")

 

步骤简述:

 

  1. 通过 Reflection 获取调用来源的 Class 实例
  2. 接着调用 Runtime 的 load0() 实现

 

load0() 首先获取系统的 SecurityManager。当 SecurityManager 存在的话检查目标 so 文件的访问权限,权限不足的话打印拒绝信息、抛出 SecurityException ,如果 name 参数为空,抛出 NullPointerException。如果 so 文件名非绝对路径的话,并不支持,并抛出 UnsatisfiedLinkError,message 为:

 

Expecting an absolute path of the library: xxx

 

针对 so 文件的权限检查和名称检查均通过的话,继续调用 ClassLoader 的 loadLibrary() 实现,需要留意的是绝对路径参数为 true。

 

// java/lang/System.java
    public static void load(String filename) {
        Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
    }

// java/lang/Runtime.java
    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);
    }

 

loadLibrary()

 

System 类提供的 loadLibrary() 用于指定 so 的名称并加载,等同于调用 Runtime 类提供的 loadLibrary()。在 Android 平台系统会自动去系统目录(/system/lib64/)、应用 lib 目录(/data/App/xxx/lib64/)下去找 libname 参数拼接了 lib 前缀的库文件。

 

The libname argument must not contAIn any platform specific prefix, file extension or path.

If a native library called libname is statically linked with the VM, then the JNI_OnLoad_libname function exported by the library is invoked.

 

Eg.

 

System.loadLibrary("A")

 

步骤简述:

 

  1. 同样通过 Reflection 获取调用来源的 Class 实例
  2. 接着调用 Runtime 的 loadLibrary0() 实现

 

loadLibrary0() 首先获取系统的 SecurityManager,并检查目标 so 文件的访问权限。权限不足或文件名为空的话和上面一样抛出 Exception。确保 so 名称不包含 /,反之抛出 UnsatisfiedLinkError,message 为:

 

Directory separator should not appear in library name: xxx

 

检查通过后,同样调用 ClassLoader 的 loadLibrary() 实现继续下一步,只不过绝对路径参数为 false。

 

// java/lang/System.java
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
    }

// java/lang/Runtime.java
    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() /

 

上面的调用栈可以看到无论是 load() 还是 loadLibrary() 最终都是调用 ClassLoader 的 loadLibrary(),主要区别在于 name 参数是 lib 完整路径、还是 lib 名称,以及是否是绝对路径参数。

 

1.首先通过 getClassLoader() 获得加载源所属的 ClassLoader 实例

 

2.确保存放 libraries 路径的字符串数组 sys_paths 不为空。尚且为空的话,调用 initializePath("java.library.path") 先初始化 usr 路径字符串数组,再调用 initializePath("sun.boot.library.path") 初始化 system 路径字符串数组。initializePath() 具体见下章节。

 

3.依据是否 isAbsolute 决定是否直接加载 library

 

name 是绝对路径的话,直接创建 File 实例,调用 loadLibrary0(),继续加载该文件。具体见下章节。检查 loadLibrary0 的结果,true 即表示加载成功,结束;false 即表示加载失败,抛出 UnsatisfiedLinkError

 

Can't load xxx

 

name 非绝对路径并且获取的 ClassLoader 存在的话,通过 findLibrary() ,根据 so 名称获得 lib 绝对路径,并创建指向该路径的 File 实例 libfile。并确保该文件的路径是绝对路径。反之,抛出 UnsatisfiedLinkError。

 

ClassLoader.findLibrary failed to return an absolute path: xxx

 

此后也是调用 loadLibrary0() 继续加载该文件,并检查 loadLibrary0 的结果,处理同上。

 

4.假使 ClassLoader 不存在,遍历 system 路径字符串数组的元素。

 

通过 mapLibraryName() 分别将 lib name 映射到平台关联的 lib 完整名称并返回,具体见下章节。创建当前遍历的 path 下 libfile 实例。调用 loadLibrary0() 继续加载该文件,并检查结果:

 

  • true 则直接结束
  • false 的话,通过 mapAlternativeName() 获取该 lib 可能存在的替代文件名,比如将后缀替换为 jnilib
    • 如果再度 map 后的 libfile 不为空,调用 loadLibrary0() 再度加载该文件并检查结果,true 则直接结束;反之,进入下一次循环

 

5.至此,如果仍未成功找到 library 文件,则在 ClassLoader 存在的情况下,到 usr 路径字符串数组中查找。

  • 遍历 usr 路径字符串数组的元素
    • 后续逻辑和上述一致,只是 map 时候的前缀不同,是 usr_paths 的元素

 

6.最终进行默认处理,即抛出 UnsatisfiedLinkError,提示在 java.library.path propery 代表的路径下也未找到 so 文件。

 

no xx in java.library.path

 

// java/lang/ClassLoader.java
    static void loadLibrary(Class<?> fromClass, String name,
                            boolean isAbsolute) {
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        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);
        }
        if (loader != null) {
            String libfilename = loader.findLibrary(name);
            if (libfilename != null) {
                File libfile = new File(libfilename);
                if (!libfile.isAbsolute()) {
                    throw new UnsatisfiedLinkError(...);
                }
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                throw new UnsatisfiedLinkError("Can't load " + libfilename);
            }
        }
        for (int i = 0 ; i < sys_paths.length ; i++) {
            File libfile = new File(sys_paths[i], System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            libfile = ClassLoaderHelper.mapAlternativeName(libfile);
            if (libfile != null && loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
        if (loader != null) {
            for (int i = 0 ; i < usr_paths.length ; i++) {
                File libfile = new File(usr_paths[i],
                                        System.mapLibraryName(name));
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                libfile = ClassLoaderHelper.mapAlternativeName(libfile);
                if (libfile != null && loadLibrary0(fromClass, libfile)) {
                    return;
                }
            }
        }
        // Oops, it failed
        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
    }

 

/ ClassLoader#initializePath() /

 

从 System 中获取对应 property 代表的 path 到数组中。

 

1.先调用 getProperty() 从 JVM 中取出配置的路径,默认的是 ""。

 

其中的 checkKey() 将检查 key 名称是否合法,null 的话抛出 NullPointerException:

 

key can't be null

 

如果为"",抛出 IllegalArgumentException:

 

key can't be empty

 

后面通过 getSecurityManager() 获取 SecurityManager 实例,检查是否存在该 property 的访问权限。

 

2.如果允许引用路径元素并且 存在的话,将路径字符串的 char 取出进行拼接、计算得到路径字符串数组。

 

3.反之通过 indexOf(/) 统计 / 出现的次数,并创建一个 / 次数 + 1 的数组。

 

4.遍历该路径字符串,通过 substring() 将各 / 的中间 path 内容提取到上述数组中。

 

5.最后返回得到的 path 数组。

 

// java/lang/ClassLoader.java
    private static String[] initializePath(String propname) {
        String ldpath = System.getProperty(propname, "");
        String ps = File.pathSeparator;
        ...

        i = ldpath.indexOf(ps);
        n = 0;
        while (i >= 0) {
            n++;
            i = ldpath.indexOf(ps, i + 1);
        }

        String[] paths = new String[n + 1];
        n = i = 0;
        j = ldpath.indexOf(ps);
        while (j >= 0) {
            if (j - i > 0) {
                paths[n++] = ldpath.substring(i, j);
            } else if (j - i == 0) {
                paths[n++] = ".";
            }
            i = j + 1;
            j = ldpath.indexOf(ps, i);
        }
        paths[n] = ldpath.substring(i, ldlen);
        return paths;
    }

 

/ ClassLoader#findLibrary() /

 

findLibrary() 将到 ClassLoader 中查找 lib,取决于各 JVM 的具体实现。比如可以看看 Android 上的实现。

 

  1. 到 DexPathList 的具体实现中调用
  2. 首先通过 System 类的 mapLibraryName() 中获得 mapping 后的 lib 全名,细节见下章节
  3. 遍历存放 native lib 路径元素数组 nativeLibraryPathElements
  4. 逐个调用各元素的 findNativeLibrary() 实现去寻找
  5. 一经找到立即返回,遍历结束仍未发现的话返回 null

 

// android/libcore/dalvik/src/main/java/dalvik/system/
// BaseDexClassLoader.java
   public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public String findLibrary(String libraryName) {
        // 到 System 中获得 mapping 后的 lib 全名
        String fileName = System.mapLibraryName(libraryName);

        // 到存放 native lib 路径数组中遍历
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);

            // 一旦找到立即返回并结束,反之进入下一次循环
            if (path != null) {
                return path;
            }
        }

        // 路径中全找遍了,仍未找到则返回 null
        return null;
    }

 

System#mapLibraryName()

 

mapLibraryName() 的作用很简单,即将 lib 名称 mapping 到完整格式的名称,比如输入 opencv 得到的是 libopencv.so。如果遇到名称为空或者长度超上限 240 的话,将抛出相应 Exception。

 

// java/lang/System.java
public static native String mapLibraryName(String libname);

 

其是 native 方法,具体实现位于 JDK Native Source Code 中,可在如下网站中看到,网站地址如下:

http://hg.openjdk.java.NET/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native

 

// native/java/lang/System.c

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

Java_java_lang_System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
{
    // 定义最后名称的 Sring 长度变量
    int len;
    // 并获取 lib 前缀、后缀的字符串常量的长度
    int prefix_len = (int) strlen(JNI_LIB_PREFIX);
    int suffix_len = (int) strlen(JNI_LIB_SUFFIX);

    // 定义临时的存放最后名称的 char 数组
    jchar chars[256];
    // 如果 libname 参数为空,抛出 NPE
    if (libname == NULL) {
        JNU_ThrowNullPointerException(env, 0);
        return NULL;
    }
    // 获取 libname 长度
    len = (*env)->GetStringLength(env, libname);
    // 如果大于 240 的话抛出 IllegalArgumentException
    if (len > 240) {
        JNU_ThrowIllegalArgumentException(env, "name too long");
        return NULL;
    }

    // 将前缀 ”lib“ 的字符拷贝到临时的 char 数组头部
    cpchars(chars, JNI_LIB_PREFIX, prefix_len);
    // 将 lib 名称从字符串里拷贝到 char 数组的 “lib” 后面
    (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
    // 更新名称长度为:前缀+ lib 名称
    len += prefix_len;
    // 将后缀 ”.so“ 的字符拷贝到临时的 char 数组里的 lib 名称后
    cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
    // 再次更新名称长度为:前缀+ lib 名称 + 后缀
    len += suffix_len;

    // 从 char 数组里提取当前长度的复数 char 成员到新创建的 String 对象中返回
    return (*env)->NewString(env, chars, len);
}

static void cpchars(jchar *dst, char *src, int n)
{
    int i;
    for (i = 0; i < n; i++) {
        dst[i] = src[i];
    }
}

 

逻辑很清晰,检查 lib 名称参数是否合法,之后便是将名称分别加上前后缀到临时字符数组中,最后转为字符串返回。

 

nativeLibraryPathElements()

 

nativeLibraryPathElements 数组来源于获取到的所有 native Library 目录后转换而来。

 

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
    }

 

所有 native Library 目录除了包含应用自身的 library 目录列表以外,还包括了系统的列表部分。

 

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    private List<File> getAllNativeLibraryDirectories() {
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
        return allNativeLibraryDirectories;
    }

    /** List of application native library directories. */
    private final List<File> nativeLibraryDirectories;

    /** List of system native library directories. */
    private final List<File> systemNativeLibraryDirectories;

 

应用自身的 library 目录列表来自于 DexPathList 初始化时传入的 librarySearchPath 参数,splitPaths() 负责去该 path 下遍历各级目录得到对应数组。

 

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    }

    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
        List<File> result = new ArrayList<>();

        if (searchPath != null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                if (directoriesOnly) {
                    try {
                        StructStat sb = Libcore.os.stat(path);
                        if (!S_ISDIR(sb.st_mode)) {
                            continue;
                        }
                    } catch (ErrnoException ignored) {
                        continue;
                    }
                }
                result.add(new File(path));
            }
        }

        return result;
    }

 

系统列表则来自于系统的 path 路径,调用 splitPaths() 的第二个参数不同,促使其在分割的时候只处理目录类型的部分,纯文件的话跳过。

 

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        ...
    }

 

拿到 path 文件列表之后就是调用 makePathElements 转成对应元素数组。

 

  1. 按照列表长度创建等长的 Element 数组
  2. 遍历 path 列表
  3. 如果 path 包含 "!/" 的话,将其拆分为 path 和 zipDir 两部分,并创建 NativeLibraryElement 实例
  4. 反之如果是目录的话,直接用 path 创建 NativeLibraryElement 实例,zipDir 参数则为空

 

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    private static NativeLibraryElement[] makePathElements(List<File> files) {
        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
        int elementsPos = 0;
        for (File file : files) {
            String path = file.getPath();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                File zip = new File(split[0]);
                String dir = split[1];
                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
            } else if (file.isDirectory()) {
                // We support directories for looking up native libraries.
                elements[elementsPos++] = new NativeLibraryElement(file);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

 

findNativeLibrary()

 

findNativeLibrary() 将先确保当 zip 目录存在的情况下内部处理 zip 的 ClassPathURLStreamHandler 实例执行了创建。

 

  • 如果 zip 目录不存在(一般情况下都是不存在的)直接判断该路径下 lib 文件是否可读,YES 则返回 path/name、反之返回 null
  • zip 目录存在并且 ClassPathURLStreamHandler 实例也创建完毕的话,检查该 name 的 zip 文件的存在。并在 YES 的情况下,在 path 和 name 之间跟上 zip 目录并返回,即:path/!/zipDir/name

 

// DexPathList.java
// android/.../libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
    private static final String zipSeparator = "!/";

    static class NativeLibraryElement {
        public String findNativeLibrary(String name) {
            // 确保 element 初始化完成
            maybeInit();

            if (zipDir == null) {
                // 如果 zip 目录为空,则直接创建该 path 下该文件的 File 实例
                // 可读的话则返回
                String entryPath = new File(path, name).getPath();
                if (IoUtils.canOpenReadOnly(entryPath)) {
                    return entryPath;
                }
            } else if (urlHandler != null) {
                // zip 目录并且 urlHandler 都存在
                // 创建该 zip 目录下 lib 文件的完整名称
                String entryName = zipDir + '/' + name;
                // 如果该名称的压缩包是否存在的话
                if (urlHandler.isEntryStored(entryName)) {
                    // 返回:路径/zip目录/lib 名称的结果出去
                    return path.getPath() + zipSeparator + entryName;
                }
            }

            return null;
        }

        // 主要是确保在 zipDir 不为空的情况下
        // 内部处理 zip 的 urlHandler 实例已经创建完毕
        public synchronized void maybeInit() {
            ...
        }
    }

 

/ ClassLoader#loadLibrary0() /

 

1.调用静态内部类 NativeLibrary 的 native 方法 findBuiltinLib() 检查是否是内置的动态链接库,细节见如下章节。如果不是内置的 library,通过 AccessController 检查该 library 文件是否存在。

 

  • 不存在则加载失败并结束
  • 存在则到本 ClassLoader 已加载 library 的 nativeLibraries Vector 或系统 class 的已加载 library Vector systemNativeLibraries 中查找是否加载过
    • 已加载过则结束
    • 反之继续加载的任务

 

2.到所有 ClassLoader 已加载过的 library Vector loadedLibraryNames 里再次检查是否加载过,如果不存在的话,抛出 UnsatisfiedLinkError:

 

Native Library xxx already loaded in another classloader

 

3.到正在加载/卸载 library 的 nativeLibraryContext Stack 中检查是否已经处理中了

 

  • 存在并且 ClassLoader 来源匹配,则结束加载
  • 存在但 ClassLoader 来源不同,则抛出 UnsatisfiedLinkError:
    • Native Library xxx is being loaded in another classloader
  • 反之,继续加载的任务

 

4.依据 ClassLoader、library 名称、是否内置等信息,创建 NativeLibrary 实例并入 nativeLibraryContext 栈。

 

5.此后,交由 NativeLibrary load,细节亦见如下章节,并在 load 后出栈。

 

6.最后根据 load 的结果决定是否将加载记录到对应的 Vector 当中。

 

// java/lang/ClassLoader.java
    private static boolean loadLibrary0(Class<?> fromClass, final File file) {
        // 获取是否是内置动态链接库
        String name = NativeLibrary.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;
            if (!exists) {
                return false;
            }
            try {
                name = file.getCanonicalPath();
            } catch (IOException e) {
                return false;
            }
        }
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        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);
                if (name.equals(lib.name)) {
                    return true;
                }
            }

            synchronized (loadedLibraryNames) {
                // 再次检查所有 library 加载历史中是否存在
                if (loadedLibraryNames.contains(name)) {
                    throw new UnsatisfiedLinkError(...);
                }
                int n = nativeLibraryContext.size();
                // 检查是否已经在加载中了
                for (int i = 0; i < n; i++) {
                    NativeLibrary lib = nativeLibraryContext.elementAt(i);
                    if (name.equals(lib.name)) {
                        if (loader == lib.fromClass.getClassLoader()) {
                            return true;
                        } else {
                            throw new UnsatisfiedLinkError(...);
                        }
                    }
                }
                // 创建 NativeLibrary 实例继续加载
                NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
                // 并在加载前后压栈和出栈
                nativeLibraryContext.push(lib);
                try {
                    lib.load(name, isBuiltin);
                } finally {
                    nativeLibraryContext.pop();
                }
                // 加载成功的将该 library 名称缓存到 vector 中
                if (lib.loaded) {
                    loadedLibraryNames.addElement(name);
                    libs.addElement(lib);
                    return true;
                }
                return false;
            }
        }
    }

 

findBuiltinLib()

 

  1. 首先一如既往地先检查 library name 是否为空,为空则抛出 Error
    1. NULL filename for native library
  2. 将 string 类型的名称转为 char 指针,失败的话抛出 OutOfMemoryError
  3. 检查名称长度是否短于最起码的 lib.so 几位,失败的话返回 NULL 结束
  4. 创建 library 名称指针 libName 并分配内存
  5. 从 char 指针提取 libxxx.so 中 xxx.so 部分到 libName 中
  6. 将 libName 中 .so 的 . 位置替换成
  7. 调用 findJniFunction() 依据 handle 指针,library 名称检查该 library 的 JNI_OnLoad() 是否存在
    1. 存在则释放 libName 内存并返回该函数地址
    2. 反之,释放内存并返回 NULL 结束

 

// native/java/lang/ClassLoader.c
Java_java_lang_ClassLoader_00024NativeLibrary_findBuiltinLib
  (JNIEnv *env, jclass cls, jstring name)
{
    const char *cname;
    char *libName;
    ...
    // 检查名称是否为空
    if (name == NULL) {
        JNU_ThrowInternalError(env, "NULL filename for native library");
        return NULL;
    }

    procHandle = getProcessHandle();
    cname = JNU_GetStringPlatformChars(env, name, 0);
    // 检查 char 名称指针是否为空
    if (cname == NULL) {
        JNU_ThrowOutOfMemoryError(env, NULL);
        return NULL;
    }

    // 检查名称长度
    len = strlen(cname);
    if (len <= (prefixLen+suffixLen)) {
        JNU_ReleaseStringPlatformChars(env, name, cname);
        return NULL;
    }
    // 提取 library 名称(取出前后缀)
    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;
    }
    if (len > prefixLen) {
        strcpy(libName, cname+prefixLen);
    }
    JNU_ReleaseStringPlatformChars(env, name, cname);
    libName[strlen(libName)-suffixLen] = '';

    // 检查 JNI_OnLoad() 释放存在
    ret = findJniFunction(env, procHandle, libName, JNI_TRUE);
    if (ret != NULL) {
        lib = JNU_NewStringPlatform(env, libName);
        free(libName);
        return lib;
    }
    free(libName);
    return NULL;
}

 

findJniFunction()

 

findJniFunction() 用于到 library 指针、已加载/卸载的 JNI 数组中查找该 library 名称所对应的 JNI_ONLOAD、JNI_ONUNLOAD 的函数地址。

 

// native/java/lang/ClassLoader.c
static void *findJniFunction(JNIEnv *env, void *handle,
                                    const char *cname, jboolean isLoad) {
    const char *onLoadSymbols[] = JNI_ONLOAD_SYMBOLS;
    const char *onUnloadSymbols[] = JNI_ONUNLOAD_SYMBOLS;
    void *entryName = NULL;
    ...
    // 如果是加载,则到 JNI_ONLOAD_SYMBOLS 中获取函数数组和长度
    if (isLoad) {
        syms = onLoadSymbols;
        symsLen = sizeof(onLoadSymbols) / sizeof(char *);
    } else {
        // 反之,则到 JNI_ONUNLOAD_SYMBOLS 中获取卸载函数数组和长度
        syms = onUnloadSymbols;
        symsLen = sizeof(onUnloadSymbols) / sizeof(char *);
    }
    // 遍历该数组,调用 JVM_FindLibraryEntry()
    // 逐个查找 JNI_On(Un)Load<_libname> function 是否存在
    for (i = 0; i < symsLen; i++) {
        // cname + sym + '_' + ''
        if ((len = (cname != NULL ? strlen(cname) : 0) + strlen(syms[i]) + 2) >
            FILENAME_MAX) {
            goto done;
        }
        jniFunctionName = malloc(len);
        if (jniFunctionName == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            goto done;
        }
        buildJniFunctionName(syms[i], cname, jniFunctionName);
        entryName = JVM_FindLibraryEntry(handle, jniFunctionName);
        free(jniFunctionName);
        if(entryName) {
            break;
        }
    }

 done:
    // 如果没有找到,默认返回 NULL
    return entryName;
}

 

JVM_FindLibraryEntry()

 

JVM_FindLibraryEntry() 调用的是平台相关的 dll_lookup(),依据 library 指针和 function 名称。

 

// vm/prims/jvm.cpp
JVM_LEAF(void*, JVM_FindLibraryEntry(void* handle, const char* name))
  JVMWrapper2("JVM_FindLibraryEntry (%s)", name);
  return os::dll_lookup(handle, name);
JVM_END

 

/ NativeLibrary#load() /

 

NativeLibrary 是定义在 ClassLoader 内的静态内部类,其代表着已加载 library 的实例,包含了该 library 的指针、所需的 JNI 版本、加载的 Class 来源、名称、是否是内置 library、是否加载过重要信息。以及核心的加载 load、卸载 unload native 实现。

 

// java/lang/ClassLoader.java
    static class NativeLibrary {
        long handle;
        private int jniVersion;
        private final Class<?> fromClass;
        String name;
        boolean isBuiltin;
        boolean loaded;

        native void load(String name, boolean isBuiltin);
        native void unload(String name, boolean isBuiltin);
        static native String findBuiltinLib(String name);
        ...
    }

 

本章节我们着重看下 load() 的关键实现:

 

1.首先调用 initIDs() 初始化 ID 等基本数据

 

  • 如果 ClassLoader$NativeLibrary 内部类、handle 等属性有一不存在的话,返回 FALSE 并结束加载
  • 通过检查的话初始化 procHandle 指针

 

2.其次通过
JNU_GetStringPlatformChars() 将 String 类型的 library 名称转为 char 类型,如果名称为空的话结束加载

 

3.如果不是内置的 so,需要调用 JVM_LoadLibrary() 加载得到指针(见下章节),反之沿用上述的 procHandle 指针即可

 

4.如果 so 指针存在的话,通过 findJniFunction() 和指针参数获取 JNI_OnLoad() 的地址。如果 JNI_OnLoad() 获取成功,则调用它并得到该 so 要求的 jniVersion。反之设置为默认值 0x00010001,即 JNI_VERSION_1_1,1.1。接着调用 JVM_IsSupportedJNIVersion() 检查 JVM 是否支持该版本,调用的是 Threads 的 is_supported_jni_version_including_1_1()。如果不支持或者是内置 so 同时版本低于 1.8,抛出 UnsatisfiedLinkError:

 

unsupported JNI version xxx required by yyy

 

反之表示加载成功。

 

5.反之,抛出异常 ExceptionOccurred。

 

// native/java/lang/ClassLoader.c
Java_java_lang_ClassLoader_00024NativeLibrary_load
  (JNIEnv *env, jobject this, jstring name, jboolean isBuiltin)
{
    const char *cname;
    ...
    void * handle;

    if (!initIDs(env)) return;

    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_t)findJniFunction(env, handle,
                                               isBuiltin ? cname : NULL,
                                               JNI_TRUE);
        if (JNI_OnLoad) {
            ...
            jniVersion = (*JNI_OnLoad)(jvm, NULL);
        } else {
            jniVersion = 0x00010001;
        }
        ...
        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;
        }
        (*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;
    }
    (*env)->SetLongField(env, this, handleID, ptr_to_jlong(handle));
    (*env)->SetBooleanField(env, this, loadedID, JNI_TRUE);

 done:
    JNU_ReleaseStringPlatformChars(env, name, cname);
}

static jboolean initIDs(JNIEnv *env)
{
    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;
        ...
        procHandle = getProcessHandle();
    }
    return JNI_TRUE;
}

 

/ JVM_LoadLibrary() /

 

JVM_LoadLibrary() 是 JVM 这层加载 library 的最后一个实现,具体步骤如下:

 

  1. 定义 1024 长度的 char 数组和接收加载结果的指针
  2. 调用 dll_load() 加载 library,其细节见下章节
  3. 加载失败的话,打印 library 名称和错误 message
  4. 同时抛出 UnsatisfiedLinkError
  5. 反之将加载结果返回

 

// vm/prims/jvm.cpp
JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
  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);
    Handle h_exception =
      Exceptions::new_exception(...);
    THROW_HANDLE_0(h_exception);
  }
  return load_result;
JVM_END

 

/ dll_load() /

 

dll_load() 的实现跟平台相关,比如 bsd 平台就是调用标准库的 dlopen(),而其最终的结果来自于 do_dlopen(),其将通过 find_library() 得到 soinfo 实例,内部将执行 to_handle() 得到 library 的指针。

 

// bionic/libdl/libdl.cpp
void* dlopen(const char* filename, int flag) {
  const void* caller_addr = __builtin_return_address(0);
  return __loader_dlopen(filename, flag, caller_addr);
}

void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
  return dlopen_ext(filename, flags, nullptr, caller_addr);
}

static void* dlopen_ext(...) {
  ScopedPthreadMutexLocker locker(&g_dl_mutex);
  g_linker_logger.ResetState();
  void* result = do_dlopen(filename, flags, extinfo, caller_addr);
  if (result == nullptr) {
    __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
    return nullptr;
  }
  return result;
}

void* do_dlopen(...) {
  ...
  if (si != nullptr) {
    void* handle = si->to_handle();
    si->call_constructors();
    failure_guard.Disable();
    return handle;
  }

  return nullptr;
}

 

/ JNI_OnLoad() /

 

JNI_OnLoad() 定义在 jni.h 中,当 library 被 JVM 加载时会回调,该方法内一般会通过 registerNatives() 注册 native 方法并返回该 library 所需的 JNI 版本。该头文件还定义了其他函数和常量,比如 JNI 1.1 等数值。

 

// jni.h
...
struct JNIEnv_ {
    ...
    jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,
                         jint nMethods) {
        return functions->RegisterNatives(this,clazz,methods,nMethods);
    }
    jint UnregisterNatives(jclass clazz) {
        return functions->UnregisterNatives(this,clazz);
    }
}
...
/* Defined by native libraries. */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);

#define JNI_VERSION_1_1 0x00010001
...

 

/ 结语 /

 

总体流程可以归纳如下:

 

  1. System 类提供的 load() 加载 so 的完整的路径名且带文件后缀,等同于直接调用 Runtime 类提供的 load();loadLibrary() 用于加载指定 so 的名称,等同于调用 Runtime 类提供的 loadLibrary()。
  2. 两者都将通过 SecurityManager 检查 so 的访问权限以及名称是否合法
  3. 之后调用 ClassLoader 类的 loadLibrary() 实现,区别在于前者指定的是否是绝对路径的 isAbsolute 参数是否为 true
  4. ClassLoader 首先需要通过 System 提供的 getProperty() 获取 JVM 配置的存放 usr、system library 路径字符串数组
  5. 如果 library name 非绝对路径,需要先调用 findLibrary() 获取该 name 对应的完整 so 文件,之后再调用 loadLibrary0() 继续
  6. 当 ClassLoader 不存在,分别到 system、usr 字符串数组中查找该 so 是否存在
  7. loadLibrary0() 将调用 native 方法 findBuiltinLib() 检查是否是内置的动态链接库,并到加载过 vector、加载中 context 中查找是否已经加载过、加载中
  8. 通过检查的话调用 NativeLibrary 静态内部类继续,事实上是调用 ClassLoader.c 的 load()
  9. 其将调用 jvm.cpp 的 JVM_LoadLibrary() 进行 so 的加载获得指针
  10. 根据 OS 的实现,dll_load() 通过 dlopen() 执行 so 的打开和地址返回
  11. 最后通过 findJniFunction() 获取 JNI_OnLoad() 地址进行 native 方法的注册和所需 JNI 版本的收集。

原文链接:
https://mp.weixin.qq.com/s/HVQvjDhhUuCrkBuOP8PJZw



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Oracle正式发布Java 22
Oracle 正式发布 Java 22,这是备受欢迎的编程语言和开发平台推出的全新版本。Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进,包括对Java 语言、其API...【详细内容】
2024-03-21  Search: Java  点击:(11)  评论:(0)  加入收藏
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  Search: Java  点击:(17)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  Search: Java  点击:(81)  评论:(0)  加入收藏
如何提高 Java 代码的可重用性
译者 | 刘汪洋审校 | 重楼对于软件开发者而言,编写可重用的代码是一项基本而重要的技能。每位工程师都应掌握如何尽可能地提高代码的复用性。当前,一些开发人员可能会认为微服...【详细内容】
2024-01-03  Search: Java  点击:(69)  评论:(0)  加入收藏
Java 21 神仙特性:虚拟线程使用指南
虚拟线程是由 Java 21 版本中实现的一种轻量级线程。它由 JVM 进行创建以及管理。虚拟线程和传统线程(我们称之为平台线程)之间的主要区别在于,我们可以轻松地在一个 Java 程序...【详细内容】
2023-12-28  Search: Java  点击:(111)  评论:(0)  加入收藏
三分钟理解 Java 虚拟线程
虚拟线程是 Java 语言中实现的一种轻量级线程,在 Java 项目中可以减少编写、维护和调试高吞吐量并发应用程序的工作量。有关虚拟线程的背景介绍,大家可以参阅 JEP 444。https:...【详细内容】
2023-12-27  Search: Java  点击:(164)  评论:(0)  加入收藏
Java Lambda 表达式各种用法,你都会了吗
Lambda表达式是 Java 8 中引入的最有影响力的功能之一。它们通过允许简洁而优雅地创建匿名函数来实现 Java 中的函数式编程。在这篇博文中,我们将探讨编写 lambda 表达式的各...【详细内容】
2023-12-25  Search: Java  点击:(101)  评论:(0)  加入收藏
别再乱用了,Java 21 将弃用、删除这些功能!
尽管Java 是我使用过的向后兼容程度最高的语言和环境之一,但始终存在功能弃用甚至删除的可能性。Java 21 将弃用两个功能,这就是我们今天要讨论的内容。1 为什么要弃用功能?弃...【详细内容】
2023-12-25  Search: Java  点击:(145)  评论:(0)  加入收藏
java 一次性处理百万数据,用了它,内存再也不会溢出了
背景最近在用一个同事写的后台管理导出数据进行数据分析,然后发现前端一直卡起,后来到服务器上查询日志,发现内存溢出了。分析出来原因:原来是表数据量过大超过百万,然后导致查出...【详细内容】
2023-12-15  Search: Java  点击:(96)  评论:(0)  加入收藏
Java 异步编程本应更简单才对
在过去的好多年里,多线程和异步一直作为 Java 技术里的高级部分,在技术序列中,一个语言分为入门部分、进阶部分和高级部分,所以,异步是作为其中的高级技术部分存在的。关于异步和...【详细内容】
2023-12-12  Search: Java  点击:(192)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(17)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(25)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(29)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(60)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(75)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(75)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(95)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(111)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(102)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(81)  评论:(0)  加入收藏
站内最新
站内热门
站内头条