作者:lds(lds2012@gmail.com)
日期:2017-04-07
?
前言
本文主要研究OpenJDK源碼中涉及到加載native本地庫的部分近刘。主要目的是為了了解本地庫是如何被加載到虛擬機肤视,如果執(zhí)行其中的本地方法镐躲,以及JNI的 JNI_OnLoad
和 JNI_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
是如何實現(xiàn)的具體的加載工作的:
首先根據(jù) libname
參數(shù)找到本地庫的文件路徑缚窿,并訪問該so庫文件來載入,
在這里會在幾個地方去找so庫文件:
- Class Loader找到的絕對路徑(class path)
-
java.library.path
定義的目錄下(windows下的PATH焰扳,linux下的LD_LIBRARY_PATH) -
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)
的參數(shù) libname
是通過調用 System.mapLibraryName
方法來將其映射為庫文件的文件名。
這個方法是一個native方法吨悍,不同系統(tǒng)有不同的實現(xiàn)扫茅,具體的區(qū)別主要在于前綴和擴展名的不同,例如在 linux 平臺下前綴和擴展名分為定義為:
#define JNI_LIB_PREFIX "lib"
#define JNI_LIB_SUFFIX ".so"
?
3. 維護本地庫列表
對于找到so庫文件以后育瓜,具體的加載工作是由 loadLibrary0
方法來完成的葫隙,
首先如果有 ClassLoader
則將本地庫加載到該 ClassLoader
的本地庫列表中,如果沒有則加載到系統(tǒng)本地庫列表中躏仇。
ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
Vector<NativeLibrary> libs =
loader != null ? loader.nativeLibraries : systemNativeLibraries;
然后遍歷已經加載的本地庫列表恋脚,如果發(fā)現(xiàn)這個本地庫已經被system或這個classLoader加載過了,則不再執(zhí)行加載工作焰手,直接返回true慧起。這里也防止了我們重復的去調用 System.loadLibrary
去加載同一個庫。
這里需要注意的是一個本地庫只能被同一個 ClassLoader
(或線程)加載册倒,一旦被某個 ClassLoader
(或線程)加載過了蚓挤,再使用另一個 ClassLoader
(或線程)去加載它,則會拋出異常。
然后的本地庫都會被封裝成 NativeLibrary
對象灿意,并存入 ClassLoader 的靜態(tài)Stack里面估灿。然后調用它的 load
方法來完成加載功能。
?
這里需要先了解一下缤剧,本地庫被誰加載馅袁,加載以后存在哪里:
首先系統(tǒng)類去維護一個本地庫列表,其中保存了由系統(tǒng)加載的本地庫名稱荒辕。
// 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<>();
最有所有的被加載過的本地庫名稱列表抵窒,以靜態(tài)變量的形式保存起來弛针。
// All native library names we've loaded.
private static Vector<String> loadedLibraryNames = new Vector<>();
然后所有的本地庫在加載后,都被以 NativeLibrary
類型保存在 ClassLoader 的靜態(tài)Stack里李皇。
?
NativeLibrary
源碼在 OpenJdk/jdk/src/share/classes/java/lang/ClassLoader.java
NativeLibrary是ClassLoader的靜態(tài)內部類削茁,用于封裝已經加載過的本地庫信息。每個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中表示內部類变勇。
這里需要說明的是最后一個參數(shù) name
,它是在構建一個 NativeLibrary
對象時傳進來的贴唇,是本地庫文件的完整路徑搀绣,其是調用 Java 中的 File.getCanonicalPath()
方法來獲取的。
?
Step 1: 先加載本地庫文件
其中最關鍵的在于根據(jù)傳入的這個 name (會將jstring類型轉換成char*類型)戳气,來加載本地庫:
handle = JVM_LoadLibrary(cname);
?
Step 2: 再執(zhí)行JNI_OnLoad函數(shù)
在其加載成功后链患,會去尋找 JNI_OnLoad
函數(shù),并執(zhí)行它瓶您, JNI_OnLoad
函數(shù)返回的其使用的JNI版本號的值麻捻,如果沒有找到該方法纲仍,則默認使用 JNI 1_1 作為版本號。
如果返回的是一個不支持的版本號贸毕,則會拋出 UnsatisfiedLinkError
異常郑叠。
?
其中 JVM_LoadLibrary
函數(shù)定義為:
OpenJdk/jdk/src/share/javavm/export 目錄下的 jvm.h
文件中:
JNIEXPORT void * JNICALL JVM_LoadLibrary(const char *name);
具體的實現(xiàn)由虛擬機在實現(xiàn),例如 hotspot 的實現(xiàn)在
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
函數(shù)明棍,它是根據(jù)系統(tǒng)不同而由不同的實現(xiàn)的乡革。
linux實現(xiàn)
例如在 linux 系統(tǒng)下的實現(xiàn)在 openjdk/hotspot/src/os/linux/vm/os_linux.cpp
文件中。
它其中主要做了兩件事情摊腋,一個是使用 linux 的 dlopen
來打開這個so本地庫文件沸版,再則檢查了這個so本地庫文件是否和當前運行虛擬機的CPU架構是否相同。
dlopen函數(shù)定義在 dlfcn.h
兴蒸,原型為:
void * dlopen( const char * pathname, int mode);
其中第二個參數(shù)使用的是 RTLD_LAZY: 異常綁定视粮。
windows實現(xiàn)
windows的實現(xiàn)是使用 LoadLibrary
函數(shù)來加載 dll 本地庫。
?
NativeLibrary_unload
Step1: 先執(zhí)行JNI_OnUnLoad方法
虛擬機在卸載本地庫文件之前类咧,會先回調本地庫文件中的 JNI_OnUnLoad
函數(shù)馒铃,可以在該函數(shù)中執(zhí)行一些清理工作蟹腾,例如清理全局變量等痕惋。
?
Step2: 再卸載本地庫文件
JVM_UnloadLibrary
和 ``JVM_loadLibrary` 函數(shù)一樣,具體根據(jù)平臺不同而實現(xiàn):
在linux平臺上娃殖,使用 dlopen
函數(shù)來 load so文件值戳, 使用 dlclose
函數(shù)來 unload.
在windows平臺上,使用 LoadLibrary
函數(shù)來load dll文件炉爆,來 FreeLibrary
函數(shù)來 unload.
?
NativeLibrary_find
尋找本地庫里的某個方法或全局變量的內存地址堕虹。
在不同平臺上的實現(xiàn)不一樣:
在linux平臺上,使用dlsym
函數(shù)來獲取某個方法的內存地址芬首。
在windows平臺上赴捞,使用 GetProcAddress
函數(shù)來獲取某個方法的內存地址。
?
?注意:在 NativeLibrary_load
和 NativeLibrary_unload
兩個函數(shù)內郁稍,不是調用了so庫里面的 JNI_OnLoad
和 JNI_OnUnLoad
函數(shù)嘛赦政,其就是使用 NativeLibrary_find
函數(shù)來找到這兩個函數(shù)地址,并執(zhí)行它們了耀怜。
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
函數(shù)里碎罚,將這個 handleID
保存到了這個 NativeLibrary
Java對象的 long handle
成員域里。每次需要使用 handleID
的時候都從這個Java對象里面的成員域去取纳像。
?
5. 加載流程小結
從整個加載本地庫的流程來看荆烈,基本上還是調用和平臺有關的函數(shù)來完成的,并在加載和卸載的時候分別調用了兩個生命周期回調函數(shù) JNI_OnLoad
和 JNI_OnUnLoad
竟趾。
以linux平臺為例憔购,簡單總結一下整個so庫的加載流程:
- 首先
System.loadLibrary()
被調用,開始整個加載過程岔帽。 - 其中調用
ClassLoader
對象來完成主要工作玫鸟,將每個本地庫封裝成NativeLibrary
對象,并以靜態(tài)變量存到已經加載過的棧中犀勒。 - 執(zhí)行
NativeLibrary
類的load
native方法屎飘,來交給native層去指向具體的加載工作。 - native層
ClassLoader.c
中的Java_java_lang_ClassLoader_00024NativeLibrary_load
函數(shù)被調用贾费。 - 在native load函數(shù)中首先使用
dlopen
來加載so本地庫文件钦购,并將返回的handle保存到NativeLibrary
對象中。 - 接著查找已經加載的so本地庫中的
JNI_OnLoad
函數(shù)褂萧,并執(zhí)行它押桃。 - 整個so本地庫的加載流程完畢。
?
只有在 NativeLibrary
對象被GC回收的時候导犹,其 finalize
方法被調用了唱凯,對應加載的本地庫才會被 unload 。這種情況一般來說并不會發(fā)生谎痢,因為 NativeLibrary
對象是以靜態(tài)變量的形式被保存的磕昼,而靜態(tài)變量是 GC roots,一般來說都不會被回收掉的节猿。
TODO: 那請問
JNI_OnUnLoad
函數(shù)什么情況下會被調用票从?虛擬機關閉的時候?一個本地庫被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