一、前言
當在CMD/SHELL中輸入 $ java Main<CR><LF> 后,Main程序就開始運行了,但在運行之前總得先把Main.class及其所依賴的類加載到JVM中吧!本篇將記錄這些日子對類加載機制的學習心得,以便日后查閱。若有紕漏請大家指正,謝謝!
以下內容均基于JDK7和HotSpot VM。
二、執行java的那刻
大家都知道通過java命令來啟動JVM和運行應用程序,但實際的流程又是如何的呢?
1. 首先根據java后的運行模式配置項或<JAVA_HOME>/jre/lib/i386/jvm.cfg來決定是以client還是server模式運行JVM,然后加載<JAVA_HOME>/jre/bin/client或server/jvm.dll,并開始啟動JVM;
2. 在啟動JVM的同時將加載Bootstrap ClassLoader(啟動類加載器,使用C/C++編寫,屬于JVM的一部分);
3. 通過Bootstrap ClassLoader加載sun.misc.Launcher類(ExtClassLoader和AppClassLoader是它的內部類);
4. sun.misc.Launcher類在執行初始化階段時,會創建一個自己的實例,在創建過程中會創建一個ExtClassLoader(擴展類加載器)實例、一個AppClassLoader(系統類加載器)實例,并將AppClassLoader實例設置為主線程的ThreadContextClassLoader(線程上下文類加載器)。
5. 然后AppClassLoader實例就開始加載Main.class及其所依賴的類庫了。
二、類加載的流程
1. 加載(Loading)
2. 鏈接(Linking),細分為:驗證(Verification)、準備(Preparation)和解析(Resolution)
3. 初始化(Initialization)
4. 使用(Using)
5. 卸載(Unloading)
注意:加載、鏈接、初始化三個階段是交叉混合進行的,并不是加載完成后才執行鏈接,也不是鏈接完成后才執行初始化的。
通過 -XX:+TraceClassLoading 可查看類加載的信息。
三、加載階段
在整個類加載機制中,僅加載階段可被程序員控制,其余階段均由JVM完全掌控。
共分為3個步驟:
1. 通過類加載器根據一個類的二進制名稱(Binary Name)獲取定義此類的二進制字節流,在讀取類的二進制字節流時鏈接階段的驗證操作的文件格式驗證已經開始,只有通過了文件格式驗證后才能存儲到方法區,若驗證失敗則拋出 java.lang.VerifyError 或其子異常類。(文件格式驗證用于保證讀取的數據能夠正確解析并存儲在JVM堆棧中的方法區。Class文件格式由JVM規范規定,而方法區的數據結構則有各JVM自行決定)
二進制字節流的來源低是多樣的,下面列舉一部分:
a. 將二進制名稱(如com.fsjohnhuang.test.Main)轉換為平臺相關的文件系統路徑(linux下為com/fsjohnhuang/test/Main.class),然后相對與類加載器查找對應的類文件;
b. 按a的做法將二進制名稱轉換為文件系統路徑,然后類加載器管轄范圍下的JAR、EAR和WAR等歸檔文件中查找類文件;
c. 通過網絡獲取二進制字節流。
2. 將字節流所代表的靜態存儲結構(Class文件結構)轉化為方法區的運行時數據結構。
3. 在內存中生成一個代表類或接口的 java.lang.Class 實例,作為操作該類或接口元數據的入口(Reflection就是利用Class實例的)。
注意:
1. 對于short boolean char int float long double基本數據類型是無需執行類加載的;
2. 對于數據類型的加載,實質上加載的是數組的組件類型(String[]數組的組件類型為String),然后由JVM內部生成一個[Ljava.lang.String的數組類型(在字節碼中標識為[Ljava/lang/String;)。因此Java中操作數組時不會像C/C++那樣出現數組越界的問題。
四、鏈接階段
鏈接階段又細分為 驗證(Verification)、準備(Preparation)和解析(Resolution) 3個子階段
解析(Resolution)不一定在類加載時執行,有可能在運行時才執行。
驗證(Verification)
驗證文件格式驗證、 元數據驗證、 字節碼驗證 和 符號引用驗證 4個操作。
1. 文件格式驗證
首先對于被反復使用和驗證過的類,驗證過程是非必要的。可以通過 -Xverify:none 來關閉驗證,可縮短虛擬機加載的時間。
操作對象:二進制字節流
目的:驗證是否符合Class文件格式的規范。
2. 元數據驗證
操作對象:方法區中的類或接口的信息
目的:對字節碼描述的類的元數據信息進行語義分析,保證符合Java語言規范。
類的元數據信息包括:
a. 父類信息(全限定名、修飾符等);
b. 父類字段、方法信息;
c. 類的信息(全限定名、修飾符等);
d. 類的字段、方法信息;
等等。注意:不含方法體信息!
3. 字節碼驗證
操作對象:方法區中的類信息的Code屬性
目的:對方法體語句進行語義分析,保證方法運行時不會出現危害JVM安全的事件
由于這種語義分析需要執行類似于下列等檢查,因此需要進行類型推導這一十分耗時的操作。
1. 檢查操作數棧的數據類型與指令的操作數類型是否兼容;
2. 檢查跳轉指令不會跳轉到方法體外的字節碼指令上;
3. 檢查類型轉換是安全的。
JDK1.6在Code屬性中添加了一個StackMapTable的屬性,用于描述方法中所有基本塊(Basic Block,按控制流拆分的代碼塊)開始時本地變量表和操作數棧引用的狀態。然后字節碼驗證時則進行類型檢查而不是類型推導,從而提高驗證的性能。可通過 -XX:-UseSplitVerifier 來關閉類型檢查回歸到類型推導,或通過 -XX:+FailOverToOldVerifier 來設置當類型檢查失敗就采用類型推導。
JDK1.7則只能采用類型檢查了。
但StackMapTable的數據依然可以被篡改,而這就是JVM開發團隊需要考慮的了。
注意:字節碼驗證時會觸發父類或所實現的接口的符號引用的解析(也就是會觸發類加載過程)。
4. 符號引用驗證
操作對象:方法區中的類或接口信息
目的:對類的符號引用和類的實際信息(類、字段、方法)進行驗證,保證符號引用可成功解析為直接引用,并當前類可以成功訪問直接引用
在執行鏈接階段的解析子階段時,會對符號引用進行符號引用驗證,驗證包括以下等內容:
a. 通過符號引用中字符串描述的全限定名是否可以在方法區中找到對應的類。
b. 通過符號引用中對字段、方法的簡單名和描述符是否可以在方法區找到對應的字段和方法。
c. 當前實例是否有權限訪問符號引用的類、字段和方法。
若驗證失敗則會拋出 java.lang.IncompatibleClassChangeError 的子類 java.lang.IllegalAccessError 、 java.lang.NoSuchFieldError 和 java.lang.NoSuchMethodError 等。
準備(Preparation)
在方法區為類變量分配內存空間,并初始化為0。示例如下:
// 經過準備階段后,value類變量將存儲在方法區中,值為0。123的賦值操作將在初始化階段進行。 public static int value = 123; // 對于類常量(類靜態常量),則直接初始化為ConstantValue屬性的值。 // 經過準備階段后,value類變量將存儲在方法區中,值為123。 public static final int value = 123;
各類型的零值
int 0 long 0L short (short)0 char '\u0000' byte (byte)0 boolean false float 0.0f double 0.0d reference null
解析(Resolution)
再次強調不一定要在類加載時執行,可以在運行時調用時才執行準備階段。
準備階段實質就是將常量池內的符號引用替換為直接引用。
符號引用(Symbolic References):以一組符號來描述所引用的目標(類、接口、方法、字段等)。只要能無歧義地定位到目標即可,并且與JVM的實際內部布局無關,而引用的目標也不一定已經加載到內存中。符號引用的形式已經由JVM規范規定了。
直接引用(Direct References): 直接引用可以是直接指向目標的指針、相對偏移量或一個能間接定位到目標的句柄。如果有了直接引用則目標必定已經在內存中存在了。
在執行newarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfiled和putstatic這16個字節碼指令執行前先對它們使用的符號引用進行解析。
除了invokedynamic指令外,其他指令觸發符號引用解析為直接引用后,將會對直接引用作緩存避免重復解析。(或者不作緩存,但JVM會保證第一解析成功則后續也會解析成功,失敗則后續解析一樣會收到相同的異常)。而invokedynamic則每次解析均不同。
解析主要針對類或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、類方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)、方法類型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)和調用點限定符(CONSTANT_InvokeDynamic_info)7種符號引用進行。(后三種是JDK1.7新增的動態語言支持信息相關)
1. 類或接口的解析
將類D中的符號引用N解析為直接引用C,首先將N的全限定名傳遞給D的類加載器去加載類C,然后進過加載、驗證、準備階段,并因為字節碼驗證而加載父類或實現的接口。一旦任何一個類或接口的加載失敗則符號引用N解析為直接應用C的操作就會被宣告失敗
成功解析后則進行符號引用驗證,檢查D是否具備訪問C的權限。若不具備則拋出`java.lang.IllegalAccessError`。
2. 字段的解析
首先對`CONSTANT_Fieldref_info`的`class_index`項所指向的符號引用進行類或接口解析。若解析成功后得到類或接口的直接引用C,則在C中查找簡單名稱和字段描述符與`CONSTANT_Fieldref_info`的`name_index`項所指向的內容相匹配的直接引用,若失敗則從下往上遞歸搜索C所實現的接口中是否有匹配的,若失敗則從下往上遞歸搜索C所實現的父類中是否有匹配的,若失敗則拋出`java.lang.NoSuchFieldError`。
若成功解析直接引用,則進行符號引用驗證,失敗則拋出`java.lang.IllegalAccessError`。
3. 類方法的解析
首先對`CONSTANT_Methodref_info`的`class_index`項所指向的符號引用進行類或接口解析。若解析成功后得到類或接口的直接引用C,則在C中查找簡單名稱和字段描述符與`CONSTANT_Methodref_info`的`name_index`項所指向的內容相匹配的直接引用,若失敗則從下往上遞歸搜索C所實現的父類中是否有匹配的,若失敗則從下往上遞歸搜索C所實現的接口中是否有匹配的(若成功說明C是一個抽象類并拋出`java.lang.AbstractMethodError`),若失敗則拋出`java.lang.NoSuchMethodError`。
若成功解析直接引用,則進行符號引用驗證,失敗則拋出`java.lang.IllegalAccessError`。
4. 接口方法的解析
首先對`CONSTANT_InterfaceMethodref_info`的`class_index`項所指向的符號引用進行接口解析。若解析成功后得到類或接口的直接引用C(若C不是接口則拋出`java.lang.IncompatibleClassChangeError`),則在C中查找簡單名稱和字段描述符與`CONSTANT_InterfaceMethodref_info`的`name_index`項所指向的內容相匹配的直接引用,若失敗則從下往上遞歸搜索C的父接口中是否有匹配的,若失敗則拋出`java.lang.NoSuchMethodError`。
五、初始化階段
類和接口均有初始化過程,實質上就是執行字節碼中的`<clinit>`構造函數。
類中靜態字段和靜態代碼塊均被代碼重排到`<clinit>`函數中進行賦值等操作。并且父類必須已經初始化后再初始化子類。
接口的靜態字段也被代碼重排到`<clinit>`函數中進行賦值操作。但不要初始化該接口前必須其父接口完成了初始化,而是在真正使用到父接口(靜態常量字段)時才觸發初始化。
JVM會自動處理多線程環境下`<clinit>`函數的同步互斥執行。因此若在`<clinit>`執行耗時的操作則會阻塞其他線程的執行。
主動引用
JVM規范規定以下5種情況,則必須執行初始化(加載、鏈接自然會在之前進入執行狀態)
1. 遇到new, getstatic, putstatic或invokestatic這4條字節碼指令時,若類沒有進行過初始化,則需要先觸發初始化。對應的Java代碼為通過關鍵字new一個實例,讀或寫一個類變量,調用類方法。
2. 使用`java.lang.reflect`包中的方法操作類時,若類沒有進行過初始化,則需要先觸發初始化。
3. 當初始化一個類時,若其父類還沒初始化則會先初始化父類。
4. 當虛擬機啟動時,虛擬機會初始化入口函數所在的類。
5. JDK1.7增加動態語言的支持。如果一個`java.lang.invoke.MethodHandle`實例最后的解析結果是REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而這個句柄所在的類沒有進行初始化,則需要先觸發初始化。
除了上述5種情況外,其他引用類的方式是不會觸發初始化的,并稱為被動引用。下列示例則為被動引用
1. 通過子類訪問父類靜態字段不會導致子類初始化,僅僅會導致父類初始化。
2. Java代碼中創建數組對象,不會導致數組的組件類(如SuperClass[]的組件類為SuperClass)初始化。因為創建數組類的字節碼指令是newarray。
3. 類A訪問類B的靜態常量不會導致類B的初始化。因為在編譯階段會將類使用到的常量直接存儲到自身常量池的引用中,因此實際上運行時類A訪問的是自身的常量與類B無關系。
六、總結
若有紕漏請大家指正,謝謝!
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4283511.html ^_^肥仔John
七、參考
《深入理解Java虛擬機 JVM高級特性與最佳實踐》
文章列表