JVM源码阅读-本地库加载流程和原理
前言
本文主要研究OpenJDK中JVM源碼中涉及到native本地庫的加載流程和原理的部分。主要目的是為了了解本地庫是如何被加載到虛擬機,以及是如何找到并執行本地庫里的本地方法,以及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?是如何實現的具體的加載工作的:
首先根據?libname?參數找到本地庫的文件路徑,并訪問該so庫文件來載入,
在這里會在幾個地方去找so庫文件:
其中可以看到,對于傳入給?System.loadLibrary(String libname)?的參數?libname?是通過調用?System.mapLibraryName?方法來將其映射為庫文件的文件名。
這個方法是一個native方法,不同系統有不同的實現,具體的區別主要在于前綴和擴展名的不同,例如在 linux 平臺下前綴和擴展名分為定義為:
#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_load?和?NativeLibrary_unload?兩個函數內,不是調用了so庫里面的?JNI_OnLoad?和?JNI_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_OnLoad?和?JNI_OnUnLoad?。
以linux平臺為例,簡單總結一下整個so庫的加載流程:
只有在?NativeLibrary?對象被GC回收的時候,其?finalize?方法被調用了,對應加載的本地庫才會被 unload 。這種情況一般來說并不會發生,因為?NativeLibrary?對象是以靜態變量的形式被保存的,而靜態變量是 GC roots,一般來說都不會被回收掉的。
TODO: 那請問?JNI_OnUnLoad?函數什么情況下會被調用?虛擬機關閉的時候?一個本地庫被load后,是否能手動的unload?什么情況下才可能被unload??
結語
參考資料,JVM源碼:
- 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
總結
以上是生活随笔為你收集整理的JVM源码阅读-本地库加载流程和原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【源码解读】Screencap源码分析-
- 下一篇: 热修复框架AndFix【源码阅读】