天天看點

Java中類的加載順序(ClassLoader)

在談ClassLoader之前,先介紹一個常見異常:ClassNotFoundExcetpion ,原因:就是找不到指定的class。

常見的場景:

  1. 調用class的forName方法時,找不到指定的類
  2. ClassLoader 中的 findSystemClass() 方法時,找不到指定的類
  3. ClassLoader 中的 loadClass() 方法時,找不到指定的類

對于這個異常,它實質涉及到了java技術體系中的類加載。Java的類加載機制是技術體系中比較核心的部分,雖然它和我們直接打交道不多,但是對其背後的機理有一定了解有助于我們排查程式中出現的類加載失敗等技術問題。

類的加載過程

一個java檔案從被加載到被解除安裝這個生命過程,總共要經曆5個階段,JVM将類加載過程分為: (加鍊初使卸)

  加載->連結(驗證+準備+解析)->初始化(使用前的準備)->使用->解除安裝

加載

首先通過一個類的全限定名來擷取此類的二進制位元組流;其次将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構;最後在java堆中生成一個代表這個類的Class對象,作為方法區這些資料的通路入口。總的來說就是查找并加載類的二進制資料。

連結:

  1. 驗證:確定被加載類的正确性;
  2. 準備:為類的靜态變量配置設定記憶體,并将其初始化為預設值;
  3. 解析:把類中的符号引用轉換為直接引用;

類的初始化

類什麼時候才被初始化

  1. 建立類的執行個體,也就是new一個對象
  2. 通路某個類或接口的靜态變量,或者對該靜态變量指派
  3. 調用類的靜态方法
  4. 反射(Class.forName(“com.kfzx.load”))
  5. 初始化一個類的子類(會首先初始化子類的父類)
  6. JVM啟動時标明的啟動類,即檔案名和類名相同的那個類

類的初始化順序

  1. 如果這個類還沒有被加載和連結,那先進行加載和連結
  2. 假如這個類存在直接父類,并且這個類還沒有被初始化(注意:在一個類加載器中,類隻能初始化一次),那就初始化直接的父類(不适用于接口)
  3. 加入類中存在初始化語句(如static變量和static塊),那就依次執行這些初始化語句。
  4. 總的來說,初始化順序依次是:(靜态變量、靜态初始化塊)–>(變量、初始化塊)–> 構造器;

如果有父類,則順序是:父類static方法 –> 子類static方法 –> 父類構造方法- -> 子類構造方法

類的加載

類的加載指的是将類的.class檔案中的二進制資料讀入到記憶體中,将其放在運作時資料區的方法區内,然後在堆區建立一個這個類的java.lang.Class對象,用來封裝類在方法區類的對象。如:

Java中類的加載順序(ClassLoader)
Java中類的加載順序(ClassLoader)

類的加載的最終産品是位于堆區中的Class對象。Class對象封裝了類在方法區内的資料結構,并且向Java程式員提供了通路方法區内的資料結構的接口。加載類的方式有以下幾種:

  1. 從本地系統直接加載
  2. 通過網絡下載下傳.class檔案
  3. 從zip,jar等歸檔檔案中加載.class檔案
  4. 從專有資料庫中提取.class檔案
  5. 将Java源檔案動态編譯為.class檔案

加載器

JVM的類加載是通過ClassLoader及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:

Java中類的加載順序(ClassLoader)
加載器介紹
  1. BootstrapClassLoader(啟動類加載器)

      負責加載​​

    ​$JAVA_HOME​

    ​​中​

    ​jre/lib/rt.jar​

    ​​裡所有的class,加載​

    ​System.getProperty(“sun.boot.class.path”)​

    ​所指定的路徑或jar。
  2. ExtensionClassLoader(标準擴充類加載器)

      負責加載java平台中擴充功能的一些jar包,包括​​

    ​$JAVA_HOME中jre/lib/*.jar​

    ​​或​

    ​-Djava.ext.dirs​

    ​​指定目錄下的jar包。加載​

    ​System.getProperty(“java.ext.dirs”)​

    ​所指定的路徑或jar。
  3. AppClassLoader(系統類加載器)

      負責記載classpath中指定的jar包及目錄中class

  4. CustomClassLoader(自定義加載器)

      屬于應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實作。

類加載器的順序
  1. 加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,隻要某個classloader已加載就視為已加載此類,保證此類隻所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
  2. 在加載類時,每個類加載器會将加載任務上交給其父,如果其父找不到,再由自己去加載。
  3. Bootstrap Loader(啟動類加載器)是最頂級的類加載器了,其父加載器為null

繼承的加載順序

由于static塊會在首次加載類的時候執行,是以下面的例子就是用static塊來測試類的加載順序。

class A{
    static{
        System.out.println("A static");
    }
}
class B extends A{
    static{
        System.out.println("B static");
    }
}
class C extends B{
    private static D d = new D();
    static{
        System.out.println("C static");
    }
}
class D{
    static{
        System.out.println("D static");
    }
}
public class ExtendTest {
    public static void main(String[] args) {
        C c = new C();
    }
}      

在上面的例子中,類C繼承B,B繼承A,而C有依賴于D。是以當建立C的時候,會自動加載C繼承的B和依賴的D,然後B又會加載繼承的A。隻有A加載完,才能順利的加載B;BD加載完,才能加載C。這就是類的加載順序了。是以程式的輸出為

A static
B static
D static
C static      

應該注意的是,C類初始化D類時有static關鍵字,如果将static關鍵字去掉,将變成

A static
B static
C static
D static      

也就是說,所有的變量初始化完,才會執行構造方法

在類的加載過程中,隻有内部的變量建立完,才會去執行這個類的構造方法。

class A2{
    B2 b2 = new B2();
    static{
        System.out.println("A static");
    }
    public A2() {
        System.out.println("A2()");
    }
}
class B2{
    C2 c2 = new C2();
    D2 d2 = new D2();
    static{
        System.out.println("B static");
    }
    public B2() {
        System.out.println("B2()");
    }
}
class C2{
    static{
        System.out.println("C static");
    }
    public C2() {
        System.out.println("C2()");
    }
}
class D2{
    static{
        System.out.println("D static");
    }
    public D2() {
        System.out.println("D2()");
    }
}
public class VarTest {
    public static void main(String[] args) {
        A2 a2 = new A2();
    }
}      

在上面的例子中,A2裡面有B2變量,B2則有C2、D2變量。是以類的加載還是先讀取到哪個,就執行相應的靜态塊。

當依賴的對象都定義完,才會執行構造方法:

A static
B static
C static
C2()
D static
D2()
B2()
A2()      

靜态成員與普通成員類的加載差別

在類的加載過程中,靜态成員類的對象,會優先加載;而普通成員類的對象則是使用的時候才回去加載。

class A3{
    B3 b3 = new B3();
    static C3 c4 = new C3();
    static{
        System.out.println("A3");
    }
}
class B3{
    static{
        System.out.println("B3");
    }
}
class C3{
    static{
        System.out.println("C3");
    }
}
public class StaticTest {
    public static void main(String[] args) {
        A3 a3 = new A3();
    }
}      

輸出:

C3
A3
B3      

類構造方法的順序

class A{
    public A() {
        System.out.println("A");
    }
}
class B extends A{
    public B() {
        System.out.println("B");
    }
}
class C extends B {
    private D d1 = new D("d1");
    private D d2 = new D("d2");
    public C() {
        System.out.println("C");
    }
}
class D {
    public D(String str) {
        System.out.println("D "+str);
    }
}
public class ExtendTest {
    public static void main(String[] args) {
        C c = new C();
    }
}      
A
B
D d1
D d2
C      
  1. 首先會調用基類的構造方法
  2. 其次,調用成員的構造方法
  3. 最後,調用自己的構造方法

總結

  1. 所有的類都會優先加載基類
  2. 靜态成員的初始化優先
  3. 成員初始化後,才會執行構造方法
  4. 靜态成員的初始化與靜态塊的執行,發生在類加載的時候。
  5. 類對象的建立以及靜态塊的通路,都會觸發類的加載。