1. 程式人生 > >深入理解JVM一載入機制

深入理解JVM一載入機制

人若無名 便可專心練劍

引子

這裡寫圖片描述
如圖,這是java程式碼到最終執行程式的過程。

1.java程式碼——>靜態編譯(javac)——>byteCode(.class)(通常為靜態編譯,除了特殊情況,如動態代理)
2.Loading——>Linking(vertify—>prepare—>resolve)——>initialization——Memory——>Execution engine(執行時)

本節我們重點關注classLoader中的載入、連線、初始化過程。

類或介面的載入過程(class loading subsystem)

這裡寫圖片描述

(以下所說的”類”等價於 java類,java介面)

Loading (載入)

“載入(Loading) ”是“類載入”(Class Loading)過程的一個階段,希望讀者沒有混淆這兩個看起來很相似的名詞。
Loading中完成的工作:
1. 通過一個類的全限定名獲取這個類對應的byteCode 二進位制流。
思考:從哪裡獲取?
通常有如下幾種方式:
- jar、war等壓縮包檔案中獲取。
- 網路中獲取二進位制流資料。如applet程式
- 動態獲取,執行時產生位元組碼,並載入。如動態代理
2. 將這個二進位制流的byteCode格式轉換為MethodArea的儲存格式,放入MethodArea。
3.

在記憶體中生成一個對應的class物件,作為方法區這個類各種資料的訪問入口。

簡而言之:載入就是把位元組碼載入MethodArea,並生成對應Class物件,以供執行使用。

連線(Linking)

連線中的具體步驟不一定是要等loading完成才能進行,一般都會在loading開始執行後交叉執行連線動作。但是,loading與linking的開始時間是按照先後的順序。

連線中有三個大的步驟:驗證——>準備——>解析
一.驗證 (verification)
驗證 (verification)為了保證 class檔案對應的位元組碼二進位制流是否符合當前JVM執行的要求,不會對JVM造成危害。驗驗證失敗丟擲java.lang.VerifyError異常。
1. 檔案格式驗證
主要驗證載入到的位元組碼流是否符合class檔案的格式規範,並且驗證是否能被當前版本的JVM處理(JVM向低版本相容),
如:
是否以魔數0xCAFEBABE開頭;
主、 次版本號是否在當前虛擬機器處理範圍之內;
常量池的常量中是否有不被支援的常量型別(檢查常量tag標誌);
指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量;CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料;
Class檔案中各個部分及檔案本身是否有被刪除的或附加的其他資訊………….

檔案格式驗證成功,則會把class檔案(bytecode)格式轉換為MethodArea的特定格式結構,接下來的步驟都會在這個MethodArea格式基礎上進行。

2.. 元資料驗證
主要是校驗元資料是否符合JVM規範。
如:
這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類);這個類的父類是否繼承了不允許被繼承的類(被final修飾的類);
如果這個類不是抽象類,是否實現了其父類或介面之中要求實現的所有方法;類中的欄位、 方法是否與父類產生矛盾(例如覆蓋了父類的final欄位,或者出現不符合規則的方法過載,例如方法引數都一致,但返回值型別卻不同等)

3.. 位元組碼驗證
主要是驗證分析方法體。

4.. 符號引用驗證
可以看做是對常量池中對各個資料的引用是否能找到對應的資料。
如:
符號引用中通過字串描述的全限定名是否能找到對應的類;
在指定類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位;
符號引用中的類、 欄位、 方法的訪問性(private、 protected、 public、 default)是否可被當前類訪問;

通常這個階段可能丟擲的異常:

java.lang.IllegalAccessError、
java.lang.NoSuchFieldError、
java.lang.NoSuchMethodError

如果確認程式碼不需要驗證(如,生成上的老程式碼),-Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間

二.準備 (prepare)

準備階段就是為static變數分配記憶體空間(MethodArea中)、並設定為初始值。

比如:

    static String bbve = "ABC";

在這個階段會給bbve變數在methodArea中分配記憶體空間,並設定為初始值(null)。在這個階段並沒有將“ABC”賦值給變數。如果用了 static final修飾,會在準備階段完成賦值。

三.解析 (resolve)
解析過程就是把class檔案常量池中的符號引用(一部分)轉換為記憶體中的直接引用的過程,這種解析完成的前提是在編譯器就可以確定要執行的具體方法、具體型別。

初始化

初始化時class載入的最後一步,之前的步驟除了loading可以由使用者自定義的classLoader載入之外,其餘都是有JVM控制的。

初始化階段開始真正的執行位元組碼,其實就是執行位元組碼的指令,執行static變數的賦值操作、執行static塊;

並且這個階段,由JVM自身保證各個class的同步(如果多
個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法), 並且一個class只會初始化一次。

雙親委派模型

這裡寫圖片描述

Bootstrap載入器負責將存放在JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的。

雙親委派模式的工作流程:

載入class二進位制流的時候,通常情況下,當前類載入器不會載入,會把載入任務委派給當前載入器的父類去載入,除了Bootstrap之外的每個載入器都是如此。所以類的載入工作總是會由Bootstrap去執行,當Bootstrap執行失敗(沒有找到這個類),才使用子類去載入。

非雙親委派模式
並不是所有程式都遵守上述模式,根據特殊場景需要,需要打破雙親委派的模式。如:使用 Thread.setContextClassLoader(ClassLoader cl) 去實現父類載入器請求子類載入器執行載入請求。(JNDI、 JDBC、 JCE、 JAXB和JBI等);OSGi中也為了特殊場景需要,改變了載入規則。可參
http://blog.onlycatch.com/post/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6