一文了解 Java 中 so 文件的加载原理
前言
无论是 Android 开发者还是 Java 工程师应该都有使用过 JNI 开发,但对于 JVM 如何加载 so、Android 系统如何加载 so,可能鲜有时间了解。
本文通过代码、流程解释,带大家快速了解其加载原理,扫清困惑。
1. System#load() + loadLibrary()
1.1 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")
步骤简述:
通过 Reflection 获取调用来源的 Class 实例
接着调用 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.javapublic static void load(String filename) {Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);}// java/lang/Runtime.javasynchronized 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);}
1.2 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")
步骤简述:
同样通过 Reflection 获取调用来源的 Class 实例
接着调用 Runtime 的 loadLibrary0() 实现
loadLibrary0() 首先获取系统的 SecurityManager,并检查目标 so 文件的访问权限:权限不足或文件名为空的话和上面一样抛出 Exception
确保 so 名称不包含 /,反之,抛出
UnsatisfiedLinkError
,message 为:Directory separator should not appear in library name: xxx
检查通过后,同样调用 ClassLoader 的 loadLibrary() 实现继续下一步,只不过绝对路径参数为 false
// java/lang/System.javapublic static void loadLibrary(String libname) {Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);}// java/lang/Runtime.javasynchronized 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);}
2. ClassLoader#loadLibrary()
上面的调用栈可以看到无论是 load()
还是 loadLibrary()
最终都是调用 ClassLoader
的 loadLibrary()
,主要区别在于 name 参数是 lib 完整路径、还是 lib 名称,以及是否是绝对路径参数。
首先通过
getClassLoader()
获得加载源所属的 ClassLoader 实例确保存放 libraries 路径的字符串数组
sys_paths
不为空- 尚且为空的话,调用 initializePath(“java.library.path”) 先初始化
usr
路径字符串数组,再调用 initializePath(“sun.boot.library.path”) 初始化system
路径字符串数组。initializePath()
具体见下章节
- 尚且为空的话,调用 initializePath(“java.library.path”) 先初始化
依据是否
isAbsolute
决定是否直接加载 libraryname 是绝对路径的话,直接创建 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 的结果,处理同上
假使 ClassLoader 不存在:遍历 system 路径字符串数组的元素,
通过
mapLibraryName()
分别将 lib name 映射到平台关联的 lib 完整名称并返回,具体见下章节创建当前遍历的 path 下 libfile 实例
调用 loadLibrary0() 继续加载该文件,并检查结果:
true 则直接结束
false 的话,通过 mapAlternativeName() 获取该 lib 可能存在的替代文件名,比如将后缀替换为
jnilib
- 如果再度 map 后的 libfile 不为空,调用 loadLibrary0() 再度加载该文件并检查结果,true 则直接结束;反之,进入下一次循环
至此,如果仍未成功找到 library 文件,则在 ClassLoader 存在的情况下,到 usr 路径字符串数组中查找
- 遍历 usr 路径字符串数组的元素
- 后续逻辑和上述一致,只是 map 时候的前缀不同,是 usr_paths 的元素
- 遍历 usr 路径字符串数组的元素
最终进行默认处理,即抛出
UnsatisfiedLinkError
,提示在 java.library.path propery 代表的路径下也未找到 so 文件
no xx in java.library.path
// java/lang/ClassLoader.javastatic 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 failedthrow new UnsatisfiedLinkError("no " + name + " in java.library.path");}
3. ClassLoader#initializePath()
从 System
中获取对应 property 代表的 path 到数组中。
先调用
getProperty()
从 JVM 中取出配置的路径,默认的是 “”其中的
checkKey()
将检查 key 名称是否合法,null
的话抛出NullPointerException
key can’t be null
如果为
""
,抛出IllegalArgumentException
key can’t be empty
后面通过
getSecurityManager()
获取SecurityManager
实例,检查是否存在该 property 的访问权限
如果允许引用路径元素并且 \ 存在的话,将路径字符串的 char 取出进行拼接、计算得到路径字符串数组
反之通过 indexOf(/) 统计 / 出现的次数,并创建一个 / 次数 + 1 的数组
遍历该路径字符串,通过 substring() 将各 / 的中间 path 内容提取到上述数组中
最后返回得到的 path 数组
// java/lang/ClassLoader.javaprivate 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;}
4. ClassLoader#findLibrary()
findLibrary() 将到 ClassLoader
中查找 lib,取决于各 JVM 的具体实现。比如可以看看 Android 上的实现。
- 到
DexPathList
的具体实现中调用 - 首先通过 System 类的
mapLibraryName()
中获得 mapping 后的 lib 全名,细节见下章节 - 遍历存放 native lib 路径元素数组
nativeLibraryPathElements
- 逐个调用各元素的
findNativeLibrary()
实现去寻找 - 一经找到立即返回,遍历结束仍未发现的话返回 null
// android/libcore/dalvik/src/main/java/dalvik/system/
// BaseDexClassLoader.javapublic String findLibrary(String name) {return pathList.findLibrary(name);}// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.javapublic 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;}}// 路径中全找遍了,仍未找到则返回 nullreturn null;}
4.1 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 参数为空,抛出 NPEif (libname == NULL) {JNU_ThrowNullPointerException(env, 0);return NULL;}// 获取 libname 长度len = (*env)->GetStringLength(env, libname);// 如果大于 240 的话抛出 IllegalArgumentExceptionif (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 名称参数是否合法,之后便是将名称分别加上前后缀到临时字符数组中,最后转为字符串返回。
4.2 nativeLibraryPathElements()
nativeLibraryPathElements 数组来源于获取到的所有 native Library 目录后转换而来。
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.javapublic DexPathList(ClassLoader definingContext, String librarySearchPath) {...this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());}
所有 native Library 目录除了包含应用自身的 library 目录列表以外,还包括了系统的列表部分。
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.javaprivate 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.javapublic 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.javapublic DexPathList(ClassLoader definingContext, String librarySearchPath) {...this.systemNativeLibraryDirectories =splitPaths(System.getProperty("java.library.path"), true);...}
拿到 path 文件列表之后就是调用 makePathElements
转成对应元素数组。
- 按照列表长度创建等长的
Element
数组 - 遍历 path 列表
- 如果 path 包含 “!/” 的话,将其拆分为 path 和 zipDir 两部分,并创建
NativeLibraryElement
实例 - 反之,如果是目录的话,直接用 path 创建 NativeLibraryElement 实例,zipDir 参数则为空
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.javaprivate 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;}
4.3 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.javaprivate 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() {...}}
5. ClassLoader#loadLibrary0()
调用静态内部类
NativeLibrary
的 native 方法findBuiltinLib()
检查是否是内置的动态链接库,细节见如下章节- 如果不是内置的 library,通过
AccessController
检查该 library 文件是否存在- 不存在则加载失败并结束
- 存在则到本 ClassLoader 已加载 library 的 nativeLibraries Vector 或系统 class 的已加载 library Vector systemNativeLibraries 中查找是否加载过
- 已加载过则结束
- 反之,继续加载的任务
- 如果不是内置的 library,通过
到所有 ClassLoader 已加载过的 library Vector
loadedLibraryNames
里再次检查是否加载过,如果不存在的话,抛出 UnsatisfiedLinkError:Native Library xxx already loaded in another classloader
到正在加载/卸载 library 的
nativeLibraryContext
Stack 中检查是否已经处理中了存在并且 ClassLoader 来源匹配,则结束加载
存在但 ClassLoader 来源不同,则抛出 UnsatisfiedLinkError:
Native Library xxx is being loaded in another classloader
反之,继续加载的任务
依据 ClassLoader、library 名称、是否内置等信息,创建 NativeLibrary 实例并入 nativeLibraryContext 栈
此后,交由 NativeLibrary load,细节亦见如下章节,并在 load 后出栈
最后根据 load 的结果决定是否将加载记录到对应的 Vector 当中
// java/lang/ClassLoader.javaprivate 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;}}}
5.1 findBuiltinLib()
首先一如既往地先检查 library name 是否为空,为空则抛出 Error
NULL filename for native library
将 string 类型的名称转为 char 指针,失败的话抛出
OutOfMemoryError
检查名称长度是否短于最起码的 lib.so 几位,失败的话返回 NULL 结束
创建 library 名称指针 libName 并分配内存
从 char 指针提取 libxxx.so 中 xxx.so 部分到 libName 中
将 libName 中 .so 的 . 位置替换成 \0
调用 findJniFunction() 依据 handle 指针,library 名称检查该 library 的 JNI_OnLoad() 是否存在
- 存在则释放 libName 内存并返回该函数地址
- 反之,释放内存并返回 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 == 0if (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] = '\0';// 检查 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;
}
5.2 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 + '_' + '\0'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:// 如果没有找到,默认返回 NULLreturn entryName;
}
5.3 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
6. NativeLibrary#load()
NativeLibrary 是定义在 ClassLoader 内的静态内部类,其代表着已加载 library 的实例,包含了该 library 的指针、所需的 JNI 版本、加载的 Class 来源、名称、是否是内置 library、是否加载过重要信息。
以及核心的加载 load、卸载 unload native 实现。
// java/lang/ClassLoader.javastatic 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() 的关键实现:
首先调用
initIDs()
初始化 ID 等基本数据- 如果 ClassLoader$NativeLibrary 内部类、handle 等属性有一不存在的话,返回 FALSE 并结束加载
- 通过检查的话初始化
procHandle
指针
其次通过
JNU_GetStringPlatformChars()
将 String 类型的 library 名称转为 char 类型,如果名称为空的话结束加载如果不是内置的 so,需要调用
JVM_LoadLibrary()
加载得到指针(见下章节),反之沿用上述的 procHandle 指针即可如果 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
反之表示加载成功
反之,抛出异常
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;
}
7. JVM_LoadLibrary()
JVM_LoadLibrary() 是 JVM 这层加载 library 的最后一个实现,具体步骤如下:
- 定义 1024 长度的 char 数组和接收加载结果的指针
- 调用
dll_load()
加载 library,其细节见下章节 - 加载失败的话,打印 library 名称和错误 message
- 同时抛出
UnsatisfiedLinkError
- 反之将加载结果返回
// 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
8. 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;
}
9. 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
...
结语
总体流程可以归纳如下:
- System 类提供的
load()
加载 so 的完整的路径名且带文件后缀,等同于直接调用 Runtime 类提供的 load();loadLibrary()
用于加载指定 so 的名称,等同于调用 Runtime 类提供的 loadLibrary()。- 两者都将通过 SecurityManager 检查 so 的访问权限以及名称是否合法
- 之后调用 ClassLoader 类的
loadLibrary()
实现,区别在于前者指定的是否是绝对路径的 isAbsolute 参数是否为 true - ClassLoader 首先需要通过 System 提供的
getProperty()
获取 JVM 配置的存放 usr、system library 路径字符串数组 - 如果 library name 非绝对路径,需要先调用
findLibrary()
获取该 name 对应的完整 so 文件,之后再调用loadLibrary0()
继续- 当 ClassLoader 不存在,分别到 system、usr 字符串数组中查找该 so 是否存在
loadLibrary0()
将调用 native 方法findBuiltinLib()
检查是否是内置的动态链接库,并到加载过 vector、加载中 context 中查找是否已经加载过、加载中- 通过检查的话调用 NativeLibrary 静态内部类继续,事实上是调用 ClassLoader.c 的
load()
- 其将调用 jvm.cpp 的
JVM_LoadLibrary()
进行 so 的加载获得指针 - 根据 OS 的实现,
dll_load()
通过dlopen()
执行 so 的打开和地址返回 - 最后通过
findJniFunction()
获取JNI_OnLoad()
地址进行 native 方法的注册和所需 JNI 版本的收集。
源码地址
- BaseDexClassLoader.java
- System.c
- ClassLoader.c
- jvm.cpp
- libdl.cpp
- jni.h
参考资料
写作本文的时候参考了很多文章的内容,也有部分内容阐述了 JNI 原理以外的东西,大家可以结合着一起看看。
- Hotspot JNI库文件加载源码解析
- JNI开发之JNI原理
- Android JNI:深入分析安卓JNI原理
- JVM的启动过程
- 深入理解JNI
- JNI/NDK入门指南之JavaVM和JNIEnv
- Android 源码中的 JNI,到底是如何使用的?
- Java System.load() 与 System.loadLibrary() 区别解析
- so加载 - Linker跟NameSpace知识
- Android NDK开发:JNI基础篇
- Android NDK开发:JNI实战篇
- JNI 从入门到实践,万字爆肝详解!
- 如何查看JVM的源码
- Where to find source code for java.lang native methods?
- Java native method source code
一文了解 Java 中 so 文件的加载原理相关推荐
- java中class文件如何加载的_jvm如何加载class文件
编译期: javac是JDK自带的编译器, 可以将java文件编译为class字节码文件, javap是JDK自带的反编译器,将.class字节码反编译为.java文件,javap -help是jav ...
- java web配置dll文件_JavaWeb项目中dll文件动态加载方法解析(详细步骤)
相信很多做Java的朋友都有过用Java调用JNI实现调用C或C++方法的经历,那么Java Web中又如何实现DLL/SO文件的动态加载方法呢.今天就给大家带来一篇JAVA Web项目中DLL/SO ...
- java 根据类名示例化类_如何使用示例从Java中的类路径加载资源
java 根据类名示例化类 Java中的类路径不仅用于加载.class文件,而且还可以用于加载资源,例如属性文件,图像,图标,缩略图或任何二进制内容. Java提供了API来将这些资源读取为Input ...
- 如何使用示例从Java中的类路径加载资源
Java中的类路径不仅用于加载.class文件,而且还可以用于加载资源,例如属性文件,图像,图标,缩略图或任何二进制内容. Java提供了API来将这些资源读取为InputStream或URL. 假设 ...
- java中成员变量的加载时机_工作奇谈——JAVA高级特性之反射
一.从问题入手 最近又要面试新人,所以翻了翻以前的代码,突然发现了一个有意思的问题. 问:如下一个Student类,请实例Student并对其成员变量赋值. public class Student ...
- Java播放midi文件及加载sf2音色库示例
最近折腾Java的MIDI功能,发现网上的教程大多只讲到怎么用Sequencer,更深入的比较难找,而且大都没的注释,于是自己踩坑无数,来这里发点稍微深入使用Java的MIDI功能的示例(嘛其实也没多 ...
- java中什么叫懒加载_java懒加载的原理
聊一下以下名词.概念或用法:lazy.lazy="extra".inverse.fetch.fetch="join".fetch=" subselec ...
- Android中dex文件的加载与优化流程
目录 1.dex文件分析...1 2.odex文件...2 2.1.odex文件结构...2 2.2.odex文件结构分析...3 3.dex文件的验证与优化...3 3.1 dex文件加载流程... ...
- vue项目中主要文件的加载顺序(index.html、App.vue、main.js)
先后顺序: index.html > App.vue的export外的js代码 > main.js > App.vue的export里面的js代码 > Index.vue的ex ...
最新文章
- 基于相机和低分辨率激光雷达的三维车辆检测
- 设置开机时自动开启和关闭的软件
- redis数据类型、应用场景、常用命令
- 谷歌Android无障碍套件,谷歌为无障碍套件添加盲文键盘:无需额外硬件就能打字...
- 基于JavaScript技术的横排文字转古书式竖排工具
- MFC状态栏编程(显示系统时间和进度条)
- 璀璨智行:V2X车路协同智慧交通
- win10专业版系统没有休眠选项如何解决?
- 常用技术指标与四大交易理论
- 51单片机课程设计:基于TCS230/3200的颜色复制显示器
- mysql时间相关函数
- oa系统怎么安装服务器配置,OA系统安装配置及维护手册-金蝶在线服务中心.DOC
- Python 实现定时任务
- CMMI基础知识扫盲 笔记
- 5ic计算机考试考卷读取错误,北京自考出现错误试卷
- 工业控制系统如何实现网络安全等级保护的相关要求
- 005_simulink建立条件子系统
- 手把手教你使用Travis CI自动部署你的Hexo博客到Github上
- 泰勒公式和麦克劳林公式
- SpringBoot集成腾讯云短信实现注册/登录功能