OpenJDK源碼學習-加載本地庫

作者:lds(lds2012@gmail.com)

日期:2017-04-07

?

前言

本文主要研究OpenJDK源碼中涉及到加載native本地庫的部分近刘。主要目的是為了了解本地庫是如何被加載到虛擬機肤视,如果執(zhí)行其中的本地方法镐躲,以及JNI的 JNI_OnLoadJNI_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庫文件:

  1. Class Loader找到的絕對路徑(class path)
  2. java.library.path 定義的目錄下(windows下的PATH焰扳,linux下的LD_LIBRARY_PATH)
  3. 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_loadNativeLibrary_unload 兩個函數(shù)內郁稍,不是調用了so庫里面的 JNI_OnLoadJNI_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_OnLoadJNI_OnUnLoad 竟趾。

以linux平臺為例憔购,簡單總結一下整個so庫的加載流程:

  1. 首先 System.loadLibrary() 被調用,開始整個加載過程岔帽。
  2. 其中調用 ClassLoader 對象來完成主要工作玫鸟,將每個本地庫封裝成 NativeLibrary 對象,并以靜態(tài)變量存到已經加載過的棧中犀勒。
  3. 執(zhí)行NativeLibrary 類的 load native方法屎飘,來交給native層去指向具體的加載工作。
  4. native層 ClassLoader.c 中的 Java_java_lang_ClassLoader_00024NativeLibrary_load 函數(shù)被調用贾费。
  5. 在native load函數(shù)中首先使用 dlopen 來加載so本地庫文件钦购,并將返回的handle保存到 NativeLibrary對象中。
  6. 接著查找已經加載的so本地庫中的 JNI_OnLoad 函數(shù)褂萧,并執(zhí)行它押桃。
  7. 整個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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末九孩,一起剝皮案震驚了整個濱河市先馆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌躺彬,老刑警劉巖煤墙,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梅惯,死亡現(xiàn)場離奇詭異,居然都是意外死亡仿野,警方通過查閱死者的電腦和手機铣减,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脚作,“玉大人葫哗,你說我怎么就攤上這事∏蛱危” “怎么了劣针?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長亿扁。 經常有香客問我捺典,道長,這世上最難降的妖魔是什么从祝? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任襟己,我火速辦了婚禮,結果婚禮上牍陌,老公的妹妹穿的比我還像新娘擎浴。我一直安慰自己,他們只是感情好呐赡,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布退客。 她就那樣靜靜地躺著骏融,像睡著了一般链嘀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上档玻,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天怀泊,我揣著相機與錄音,去河邊找鬼误趴。 笑死霹琼,一個胖子當著我的面吹牛,可吹牛的內容都是我干的凉当。 我是一名探鬼主播枣申,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼看杭!你這毒婦竟也來了忠藤?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤楼雹,失蹤者是張志新(化名)和其女友劉穎模孩,沒想到半個月后尖阔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡榨咐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年介却,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片块茁。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡齿坷,死狀恐怖,靈堂內的尸體忽然破棺而出数焊,到底是詐尸還是另有隱情胃夏,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布昌跌,位于F島的核電站仰禀,受9級特大地震影響,放射性物質發(fā)生泄漏蚕愤。R本人自食惡果不足惜答恶,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萍诱。 院中可真熱鬧悬嗓,春花似錦、人聲如沸裕坊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽籍凝。三九已至捌袜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間相寇,已是汗流浹背脏款。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留退盯,地道東北人彼乌。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像渊迁,于是被迫代替她去往敵國和親慰照。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內容