漫談JVM之類加載機制(篇一)


前言 最近在看一本書,發現代碼里用到了Thread.currentThread().getContextClassLoader(),為什么類加載器還與線程有關系呢,為什么不直接使用ClassLoader.getSystemClassLoader()呢?帶着這些疑問又把JVM類加載機制從頭到尾學習了一遍。

篇一 類加載時機

我們編寫的代碼存儲在java文件中,java源代碼通過編譯生成Java虛擬機可識別的字節碼,存儲在Class文件中。運行java程序時需要將Class文件中的信息加載到Java虛擬機中,而這個過程就是類加載的過程。
JVM類加載示例

如上圖所示,假設寫一個類A存儲為A.java,通過javac A.java編譯生成A.class,A.class中存儲了各種描述A類的信息。然后運行程序,執行java A.class,這時java虛擬機會先將A.class中信息轉換成Java虛擬機所需的存儲格式,然后存放在方法區中。之后,Java虛擬機會再創建一個java.lang.Class對象實例,這個實例將作為訪問方法區中A類信息的入口。使用A中方法時,要先創建一個實例new A(),Java虛擬機基於類的描述信息在Java堆中創建一個A的實例。

那何時會觸發類的加載呢?Java虛擬機規范中並未明確指出,但對類的初始化時機做了明確說明。這兩者有什么關系呢,我們先了解一下類的加載流程:
JVM類加載流程
根據這個流程,初始化觸發時類加載的第一個階段---加載階段肯定已經完成了,那我們可以這樣推論,類初始化的觸發時機定會觸發整個類加載過程。

上面示例中提到的新建一個對象實例會先加載對象,java虛擬機規范中提到在以下五種情況將觸發類的初始化[1]

1) 遇到new,getstatic,putstatic,invokestatic這四條字節碼指令時;

為了驗證我們先設置個基礎類B,提供了一個靜態字段,一個靜態塊。靜態方法和靜態塊會在編譯期匯集到<clinit>類初始化方法中,固可以用這個方法的運行結果引證類的初始化。

  1. public class B
  2. public static int f; 
  3. static
  4. System.out.println("init B"); 

  5. public static void m()
  6. System.out.println("invoke m"); 


  • new:新建對象;

  1. public class C
  2. public static void main(String args[])
  3. new B(); 


運行結果:

  1. init B 

說明類已經初始化了

  • pustatic:設置類的靜態字段;

  1. public class C
  2. public static void main(String args[])
  3. B.f=5


運行結果:

  1. init B 
  • getstatic:獲取類的靜態字段;

  1. public class C
  2. public static void main(String args[])
  3. System.out.println(B.f); 


運行結果

  1. init B 
  2. 0 
  • invokestatic:調用類的靜態方法;

  1. public class C
  2. public static void main(String args[])
  3. B.m(); 


運行結果:

  1. init B 
  2. invoke m 

2) 使用java.lang.reflect包或Class的方法對類進行反射調用時;

  1. public class C
  2. public static void main(String args[])
  3. try
  4. Class.forName("B"); 
  5. } catch (ClassNotFoundException e) { 



運行結果:

  1. init B 

3) 當初始化一個類,其父類還沒有初始化時;

這里我們新建一個類A繼承B,通過初始化A看看B有沒有初始化。

  1. public class A extends B
  2. static
  3. System.out.println("init A"); 


  4.  
  5. public class C
  6. public static void main(String args[])
  7. new A(); 


運行結果:

  1. init B 
  2. init A 

發現B也初始化了,並且是B先初始化完A才初始化,也就是初始化一個類時會先看其父類是否已經初始化,依次類推一直到java.lang.Object。其實除了類需要初始化,接口也需要初始化,用於初始化接口變量的賦值。與類的初始化不同,接口初始化時並不會遞歸初始化所有父接口,而是用到哪個接口就初始化哪個接口,如調用接口中的常量。由於接口中不允許有靜態塊,那<clinit>就只用於初始化其常量。
新建兩個接口E、G,類F實現這兩個接口:

  1. public interface E
  2. B b = new B();//依據接口的特性 默認變量static final修飾 

  3. public interface G
  4. D d = new D(); 

  5. public class D
  6. static
  7. System.out.println("init D"); 


  8. public class B
  9. static
  10. System.out.println("init B"); 


  11. public class F implements E,G
  12. //只為了測試這個方法無需任何實現 

  13. public class C
  14. public static void main(String args[])
  15. System.out.println(F.b); 


這里運行結果只有init B而沒有init D說明沒有觸發接口G的初始化。

4) 當Java虛擬機啟動時,用戶指定的主類(包含main方法的類)要先進行初始化;

程序運行一定會有一個入口,也就是Main類,java虛擬機啟動時會現將其初始化。下面我們在B類里加一個空的main方法,運行看一下效果:

  1. public class B
  2. public static int f; 
  3. static
  4. System.out.println("init B"); 

  5. public static void m()
  6. System.out.println("invoke m"); 

  7. public static void main(String args[])


編譯后運行java B控制台輸出init B,說明B也初始化完成了。

5) 在初次調用java.lang.invoke.MethodHandle實例時,通過java虛擬機解析出類型是REF_getStatic,REF_puStatic,REF_invokeStatic的方法句柄時;

注意,上面所說的場景都有一個前提就是對應的類沒有初始化過,如果這個類已經初始化了,直接使用就可以了。

以上這些場景都屬於對一個類的主動引用,除了這些場景外其他引用類的方式都不會觸發初始化。我們從上面的場景用找幾個特例來看一下是否能使其初始化:

1)針對第一條,通過子類調用父類的靜態變量或靜態方法

這里嚴重上面第三條的類A,類B

  1. public class C
  2. public static void main(String args[])
  3. A.f=5


通過類A賦值類B的靜態字段f,運行結果只有init B,而沒有初始化類A,因為這里用到的是類變量,只是借用了A對B的繼承關系,無需對A進行初始化。

2)調用類中的常量

常量在編譯階段會直接在調用類中將常量值存入其常量池中,與被調用類其實也沒有關系了,固對常量的調用並不會引起被調用類的初始化,如下:

  1. public class D
  2. public static final String f = "f"
  3. static
  4. System.out.println("init D"); 


  5.  
  6. public class C
  7. public static void main(String args[])
  8. System.out.println(D.f); 


運行結果:


證實了上面的說法

3) 通過數組引用類

對於數組java虛擬機會特殊處理,在執行時Java虛擬機會動態生成一個數組對象,這時初始化的只是這個數組對象。當使用數組的元素時才會真正觸發元素類型的初始化。
直接在main方法中新建數組:

  1. public class H
  2. public static void main(String args[])
  3. B[] bs = new B[1]; 


運行結果什么也沒有輸出,說明B沒有初始化。通過輸出bs發現這是一個[LB對象,是由newarray指令動態生成的。

  1. public class H
  2. public static void main(String args[])
  3. B[] bs = new B[1]; 
  4. System.out.println("-------"); 
  5. System.out.println(bs[0].f); 


運行結果:

  1. ------- 
  2. init B 
  3. 0 

這說明直到bs[0].f時才真正觸發B的初始化。

寫了這么多才發現僅僅談到類加載的時機,離着解決篇頭的問題還差一大截。沒辦法,要想徹底了解清楚類加載必須慢下心一步一步來。

若發現文章中任何問題,歡迎指正,互相學習。


  1. 這里說的初始化指的是類初始化,還有實例初始化是在創建實例時進行的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM