開篇
最近剛剛看了Uber開源的JVM Profiler的源碼,對裡面的修改位元組碼的流程有了一定的認識,剛好之前看到網上有人寫了一篇關于java類加載時機與過程的文章,想了想決定把兩者合并起來寫一下。概念比較基礎,有興趣的可以看看。
類加載過程
- 加載
加載(Loading)階段是“類加載”(Class Loading)過程的第一個階段,在此階段,虛拟機需要完成以下三件事情:
1、 通過一個類的全限定名來擷取定義此類的二進制位元組流。
2、 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
3、 在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些資料的通路入口。
加載階段即可以使用系統提供的類加載器在完成,也可以由使用者自定義的類加載器來完成。
加載階段與連接配接階段的部分内容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,加載階段尚未完成,連接配接階段可能已經開始。
- 驗證
驗證是連接配接階段的第一步,這一階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。
- 準備
準備階段是為類的靜态變量配置設定記憶體并将其初始化為預設值,這些記憶體都将在方法區中進行配置設定。
準備階段不配置設定類中的執行個體變量的記憶體,執行個體變量将會在對象執行個體化時随着對象一起配置設定在Java堆中。
public static int value=123;
在準備階段value初始值為0 。在初始化階段才會變為123 。
- 解析
解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程。
- 初始化
類初始化是類加載過程的最後一步,前面的類加載過程,除了在加載階段使用者應用程式可以通過自定義類加載器參與之外,其餘動作完全由虛拟機主導和控制。
到了初始化階段,才真正開始執行類中定義的Java程式代碼。
初始化階段是執行類構造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中的所有類變量的指派動作和靜态語句塊(static{}塊)中的語句合并産生的。
類初始化時機:
1、建立類的執行個體
2、通路類的靜态變量(除常量【被final修辭的靜态變量】原因:常量一種特殊的變量,因為編譯器把他們當作值(value)而不是域(field)來對待。
3、通路類的靜态方法
4、反射如(Class.forName("my.xyz.Test"))
5、當初始化一個類時,發現其父類還未初始化,則先出發父類的初始化
6、虛拟機啟動時,定義了main()方法的那個類先初始化
一個案例
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
準備階段
類加載的時候在準備過程中為類的靜态變量配置設定記憶體并初始化預設值
靜态變量初始化結果
singleton=null
count1=0,
count2=0
初始化階段
在通路類的靜态方法的時候會初始化(上例子中執行SingleTon singleTon = SingleTon.getInstance())
執行指派SingleTon singleTon = new SingleTon()
private SingleTon() {
count1++; //count1在加載過程中初始化為0,count1++變為1
count2++; //count2在加載過程中初始化為0,count2++變為1
}
執行指派count2=0 後 count2變為0
執行結果
count1=1
count2=0
java agent攔截階段
可以在加載java檔案之前做攔截把位元組碼做修改