前言 最近在看一本書,發現代碼里用到了
Thread.currentThread().getContextClassLoader(),為什么類加載器還與線程有關系呢,為什么不直接使用ClassLoader.getSystemClassLoader()呢?帶着這些疑問又把JVM類加載機制從頭到尾學習了一遍。
篇一 類加載時機
我們編寫的代碼存儲在java文件中,java源代碼通過編譯生成Java虛擬機可識別的字節碼,存儲在Class文件中。運行java程序時需要將Class文件中的信息加載到Java虛擬機中,而這個過程就是類加載的過程。

如上圖所示,假設寫一個類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虛擬機規范中並未明確指出,但對類的初始化時機做了明確說明。這兩者有什么關系呢,我們先了解一下類的加載流程:

根據這個流程,初始化觸發時類加載的第一個階段---加載階段肯定已經完成了,那我們可以這樣推論,類初始化的觸發時機定會觸發整個類加載過程。
上面示例中提到的新建一個對象實例會先加載對象,java虛擬機規范中提到在以下五種情況將觸發類的初始化[1]:
1) 遇到new,getstatic,putstatic,invokestatic這四條字節碼指令時;
為了驗證我們先設置個基礎類B,提供了一個靜態字段,一個靜態塊。靜態方法和靜態塊會在編譯期匯集到<clinit>類初始化方法中,固可以用這個方法的運行結果引證類的初始化。
- public class B {
- public static int f;
- static {
- System.out.println("init B");
- }
- public static void m(){
- System.out.println("invoke m");
- }
- }
-
new:新建對象;
- public class C{
- public static void main(String args[]) {
- new B();
- }
- }
運行結果:
- init B
說明類已經初始化了
-
pustatic:設置類的靜態字段;
- public class C{
- public static void main(String args[]) {
- B.f=5;
- }
- }
運行結果:
- init B
-
getstatic:獲取類的靜態字段;
- public class C{
- public static void main(String args[]) {
- System.out.println(B.f);
- }
- }
運行結果
- init B
- 0
-
invokestatic:調用類的靜態方法;
- public class C{
- public static void main(String args[]) {
- B.m();
- }
- }
運行結果:
- init B
- invoke m
2) 使用java.lang.reflect包或Class的方法對類進行反射調用時;
- public class C{
- public static void main(String args[]) {
- try {
- Class.forName("B");
- } catch (ClassNotFoundException e) {
- }
- }
- }
運行結果:
- init B
3) 當初始化一個類,其父類還沒有初始化時;
這里我們新建一個類A繼承B,通過初始化A看看B有沒有初始化。
- public class A extends B {
- static {
- System.out.println("init A");
- }
- }
-
- public class C{
- public static void main(String args[]) {
- new A();
- }
- }
運行結果:
- init B
- init A
發現B也初始化了,並且是B先初始化完A才初始化,也就是初始化一個類時會先看其父類是否已經初始化,依次類推一直到java.lang.Object。其實除了類需要初始化,接口也需要初始化,用於初始化接口變量的賦值。與類的初始化不同,接口初始化時並不會遞歸初始化所有父接口,而是用到哪個接口就初始化哪個接口,如調用接口中的常量。由於接口中不允許有靜態塊,那<clinit>就只用於初始化其常量。
新建兩個接口E、G,類F實現這兩個接口:
- public interface E {
- B b = new B();//依據接口的特性 默認變量static final修飾
- }
- public interface G {
- D d = new D();
- }
- public class D {
- static {
- System.out.println("init D");
- }
- }
- public class B {
- static {
- System.out.println("init B");
- }
- }
- public class F implements E,G {
- //只為了測試這個方法無需任何實現
- }
- public class C {
- public static void main(String args[]) {
- System.out.println(F.b);
- }
- }
這里運行結果只有init B而沒有init D說明沒有觸發接口G的初始化。
4) 當Java虛擬機啟動時,用戶指定的主類(包含main方法的類)要先進行初始化;
程序運行一定會有一個入口,也就是Main類,java虛擬機啟動時會現將其初始化。下面我們在B類里加一個空的main方法,運行看一下效果:
- public class B {
- public static int f;
- static {
- System.out.println("init B");
- }
- public static void m(){
- System.out.println("invoke m");
- }
- 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
- public class C{
- public static void main(String args[]) {
- A.f=5;
- }
- }
通過類A賦值類B的靜態字段f,運行結果只有init B,而沒有初始化類A,因為這里用到的是類變量,只是借用了A對B的繼承關系,無需對A進行初始化。
2)調用類中的常量
常量在編譯階段會直接在調用類中將常量值存入其常量池中,與被調用類其實也沒有關系了,固對常量的調用並不會引起被調用類的初始化,如下:
- public class D {
- public static final String f = "f";
- static {
- System.out.println("init D");
- }
- }
-
- public class C{
- public static void main(String args[]){
- System.out.println(D.f);
- }
- }
運行結果:
- f
證實了上面的說法
3) 通過數組引用類
對於數組java虛擬機會特殊處理,在執行時Java虛擬機會動態生成一個數組對象,這時初始化的只是這個數組對象。當使用數組的元素時才會真正觸發元素類型的初始化。
直接在main方法中新建數組:
- public class H {
- public static void main(String args[]){
- B[] bs = new B[1];
- }
- }
運行結果什么也沒有輸出,說明B沒有初始化。通過輸出bs發現這是一個[LB對象,是由newarray指令動態生成的。
- public class H {
- public static void main(String args[]){
- B[] bs = new B[1];
- System.out.println("-------");
- System.out.println(bs[0].f);
- }
- }
運行結果:
- -------
- init B
- 0
這說明直到bs[0].f時才真正觸發B的初始化。
寫了這么多才發現僅僅談到類加載的時機,離着解決篇頭的問題還差一大截。沒辦法,要想徹底了解清楚類加載必須慢下心一步一步來。
若發現文章中任何問題,歡迎指正,互相學習。
-
這里說的初始化指的是類初始化,還有實例初始化是在創建實例時進行的。 ↩
