Class文件的加載過程java
ClassLoader的工做模式數據結構
類的熱加載函數
1 Class文件的裝載流程佈局
只有被java虛擬機裝載的Class類型才能在程序中使用(注意裝載和加載的區別)優化
1.1 類裝載的條件日誌
Class只有在必需要使用的時候纔會被裝載,Java虛擬機不會無條件的裝載Class類型。Java虛擬機規定:一個類或者接口在初次使用時,必須進行初始化。這裏的使用指的是主動使用,主動使用有如下幾種狀況:對象
除了以上狀況屬於主動使用外,其餘狀況均屬於被動使用,被動使用不會引發類的初始化。blog
例1:主動使用接口
public class Parent{內存
static{
System.out.println("Parent init");
}
}
public class Child{
static{
System.out.println("Child init");
}
}
public class InitMain{
public static void main(String[] args){
Child c = new Child();
}
}
以上聲明瞭3個類:Parent Child InitMain,Child類爲Parent類的子類。若Parent被初始化,根據代碼中的static塊可知,將會打印"Parent init",若Child被初始化,則會打印"Child init"。執行InitMain,結果爲:
Parent init
Child init
由此可知,系統首先裝載Parent類,接着裝載Child類。符合主動裝載中的兩個條件,使用new關鍵字建立類的實例會裝載相關的類,以及在初始化子類時,必須先初始化父類。
例2 :被動裝載
public class Parent{
static{
System.out.println("Parent init ");
}
public static int v = 100; //靜態字段
}
public class Child extends Parent{
static{
System.out.println("Child init");
}
}
public class UserParent{
public static void main(String[] args){
System.out.println(Child.v);
}
}
Parent中有靜態變量v,而且在UserParent中,使用其子類Child去調用父類中的變量。
運行代碼:
Parent init
100
雖然在UserParent中,直接訪問了子類對象,可是Child子類並未初始化,只有Parent父類進行初始化。因此,在引用一個字段時,只有直接定義該字段的類,纔會被初始化。
注意:雖然Child類沒有被初始化,可是,此時Child類已經被系統加載,只是沒有進入初始化階段。
能夠使用-XX:+ThraceClassLoading 參數運行這段代碼,查看日誌,即可以看到Child類確實被加載了,只是初始化沒有進行
例3 :引用final常量
public class FinalFieldClass{
public static final String constString = "CONST";
static{
System.out.println("FinalFieldClass init");
}
}
public class UseFinalField{
public static void main(String[] args){
System.out.println(FinalFieldClass.constString);
}
}
運行代碼:CONST
FinalFieldClass類沒有由於其常量字段constString被引用而初始化,這是由於在Class文件生成時,final常量因爲其不變性,作了適當的優化。
分析UseFinalField類生成的Class文件,能夠看到main函數的字節碼爲:
在字節碼偏移3的位置,經過Idc將常量池第22項入棧,在此Class文件中常量池第22項爲:
#22 = String #23 //CONST
#23 = UTF8 CONST
由此能夠看出,編譯後的UseFinalField.class中,並無引用FinalFieldClass類,而是將其final常量直接存放在常量池中,所以,FinalFiledClass類天然不會被加載。(javac在編譯時,將常量直接植入目標類,再也不使用被引用類)經過捕獲類加載日誌(部分日誌)能夠看出:
注意:並非在代碼中出現的類,就必定會被加載或者初始化,若是不符合主動使用的條件,類就不會被初始化。
1.2 類裝載的整個過程
1)加載類
加載類處於類裝載的第一個階段。
加載類時,JVM必須完成:
2)鏈接
1 驗證類:
當類被加載到系統後,就開始鏈接操做,驗證是鏈接的第一步。
主要目的是保證加載的字節碼是符合規範的。驗證的步驟如圖:
2 準備
當一個類驗證經過後,虛擬機就會進入準備階段,在這個階段,虛擬機會爲這個類分配相應的內存空間,並設置初始值。
java虛擬機爲各類類型變量默認的初始值如表:
類型 | 默認初始值 |
int | 0 |
long | 0L |
short | (short)0 |
char | \u0000 |
boolean | false |
reference | null |
float | 0f |
double | 0f |
注意:java並不支持boolean類型,對於boolean類型,內部實現是Int,因爲int的默認值是0,故對應的,boolean的默認值是false
若是類屬於常量字段,那麼常量字段也會在準備階段被附上正確的值,這個賦值屬於java虛擬機的行爲,屬於變量的初始化。事實上,在準備階段,不會有任何java代碼被執行。
3 解析類
在準備階段完成後,就進入瞭解析階段。
解析階段的任務就是將類、接口、字段和方法的符號引用轉爲直接引用。
符號引用就是一些字面量的引用,和虛擬機的內部數據結構和內存佈局無關。比較容易理解的就是在Class類文件中,經過常量池進行大量的符號引用。
具體能夠使用JclassLib軟件查看Class文件的結構:::
3)初始化
初始化時類裝載的最後一個階段。若是前面的步驟沒有出現問題,那麼表示類能夠順利裝載到系統中。此時,類纔會開始執行java字節碼。
初始化階段的重要工做是執行類的初始化方法<clinit>。方法<clinit>是由編譯器自動生成的,它是由類靜態成員的賦值語句以及static語句塊合併產生的。
例如:
public class SimpleStatic{
public static int id = 1;
public static int number;
static{
number = 4;
}
}
java編譯器爲這段代碼生成以下的<clinit>:
0 iconst_1
1 putstatic #2 <Demo.id>
4 iconst_4
5 putstatic #3 <Demo.number>
8 return
能夠看出,生成的<clinit>函數中,整合了SimpleStatic類中的static賦值語句以及static語句塊,前後對id和number兩個成員變量進行賦值
因爲在加載一個類以前,虛擬機老是會試圖加載該類的父類,所以父類的<clinit>老是在子類<clinit>以前被調用。也就是說,子類的static塊優先級高於父類。
public class ChildStatic extends Demo{
static{
number = 2;
}
public static void main(String[] args){
System.out.println(number);
}
}
運行可知:
2
說明父類的<clinit>老是在子類<clinit>以前被調用。
注意:java編譯器並非爲全部的類都產生<clinit>初始化函數,若是一個類既沒有賦值語句,也沒有static語句塊,那麼生成的<clinit>函數就應該爲空,所以,編譯器就不會爲該類插入<clinit>函數
例如:
public class StaticFinalClass{
public static final int i=1;
public static final int j=2;
}
因爲StaticFinalClass只有final常量,而final常量在準備階段初始化,而不在初始化階段處理,所以對於StaticFinalClass類來講,<clinit>就無事可作,所以,在產生的class文件中沒有該函數存在。