Java 類的加載,鏈接,初始化

更多 Java 虛擬機方面的文章,請參見文集《Java 虛擬機》


一個類 Person 從代碼到使用:

  • 編譯器負責將 Person.java 源文件編譯為 Person.class 字節碼文件
  • 類加載器 Class Loader 負責將 Person.class 字節碼 (表現形式為字節數組 byte[])轉換為 JVM 中的 Class<Person> 對象
  • 隨后 JVM 再利用 Class<Person> 對象 實例化為 Person 對象

1. 類的加載

1.1 類加載器 Class Loader

作用:

  • 將 .class 文件中的字節碼轉換為 JVM 中的 Class 對象(不是 Class 的實例)
  • 為 JVM 中相同名稱的類創建隔離空間。使得同一名稱不同版本的兩個 Java 類可以在 JVM 中同時存在,例如 OSGI。
    在 JVM 中判斷兩個類是否相同:類的二進制名稱相同 并且 類加載器相同

類加載器 Class Loader 具有層次組織結構,即每個類加載器都有一個父類加載器,通過 getParent() 可以獲得父類加載器。

類加載器 Class Loader 使用代理模式,每個類加載器即可以自己完成 Java 類的定義工作,也可以代理給其他的類加載器來完成。

  • 初始類加載器:啟動一個類的加載過程
  • 定義類加載器:負責最終定義這個類。
    例如在下面的代碼中,A 的定義類加載器負責啟動 B 的加載過程
class A {
    private B b;
}

1.2 類加載器 Class Loader 的加載策略

  • 類加載器在嘗試自己去加載某個類之前,會首先代理給父類加載器。當父類加載器在 class path 中找不到對應的 .class 字節碼文件時,才會嘗試自己加載。
    一般的 Java 應用使用該策略。從 ClassLoaderloadClass() 方法中可以看出 c = parent.loadClass(name, false);
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • 相反策略,類加載器首先嘗試自己去加載某個類,當其在 class path 中找不到對應的 .class 字節碼文件時,再代理給父類加載器。
    該策略在 Java Web 容器中比較常見。Apache Tomcat 為每個 Application 提供一個獨立的類加載器 WebappClassLoader,使得 Application 自己的類的優先級高于 Web 容器提供的類,因此不同的 Application 可以使用不同版本的庫。

1.3 JVM 自帶的 Class Loader

  • SystemClassLoader:C++編寫,加載核心庫 java.*
  • ExtClassLoader:Java編寫,加載擴展庫 javax.*
  • AppClassLoader:Java編寫,加載程序所在目錄

通過 Thread.currentThread().getContextClassLoader() 獲得當前類加載器

public static void main(String[] args) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    System.out.println(cl.toString());

    try {
        Class c = cl.loadClass("jvm.Person");
        System.out.println(c.getClassLoader());
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

輸出:

sun.misc.LauncherAppClassLoader@75b84c92 sun.misc.LauncherAppClassLoader@75b84c92

1.4 自定義類加載器 Class Loader

繼承父類 ClassLoaderClassLoader 中包含如下方法:

  • final Class<?> defineClass(String name, byte[] b, int off, int len)
    • 將字節碼數組轉換為 Class<?> 對象
    • 該方法不能被 override
  • final Class<?> findLoadedClass(String name)
    • 查找已經加載過的 Class<?> 對象,即 Java 類
    • 一個類加載器不會重復加載同一個類
    • 該方法不能被 override
  • Class<?> findClass(String name)
    • 根據名稱查找并加載 Java 類
    • 該方法需要被 override
  • Class<?> loadClass(String name)
    • 根據名稱加載 Java 類
    • 該方法不能被 override

例如我們可以自定義一個類加載器負責從網絡中獲取字節碼,并轉化為 Class 對象。

class MyClassLoader extends ClassLoader {
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = new byte[1024];
        // 從 網絡中根據 name 讀取 字節數組

        // 將字節碼數組轉換為 Class<?> 對象
        return defineClass(name, bytes, 0, bytes.length);
    }
}

1.5 顯示加載 VS 隱式加載

  • 顯示加載:
    • 通過 Class c = Class.forName("Student");
    • 通過 ClassLoader 的 loadClass() 方法,例如:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = cl.loadClass("Student");

2. 類的鏈接

類的鏈接:將 Java 類的二進制代碼合并到 JVM 的運行狀態之中的過程。

包括三個步驟:

  • 驗證:確保 Java 類的二進制表示在結構上是合理的
  • 準備:創建靜態域并賦值
  • 解析:確保當前類引用的其他類被正確地找到,該過程可能會觸發其他類被加載。

關于解析,不同的 JVM 有不同的解析策略,例如:

public class A {
  public void main(String args[]) {
    B b = null;
  }
}
  • 策略1:鏈接 A 的時候發現引用了 B,因此加載 B
  • 策略2:鏈接 A 的時候發現引用了 B,但是 B 沒有被使用,因此不加載 B。在真正使用 B 時才加載 B,例如 b = new B();

3. 類的初始化

類的初始化:當 Java 類第一次被真正使用的時候,JVM 會負責初始化該類。包括:

  • 執行靜態代碼塊
  • 初始化靜態域

注意:是類的初始化,不是對象的初始化。

例如:下面的代碼不會初始化類 A,因為 A 沒有真正被使用。

public static void main(String[] args) {
    A a;
}

static class A {
    static int i = 10;
    static {
        System.out.println("Init class A");
    }
}

例如:下面的代碼會初始化類 A,因為 A 真正被使用,輸出 Init class A

public static void main(String[] args) {
    int i = A.i;
}

static class A {
    static int i = 10;
    static {
        System.out.println("Init class A");
    }
}

引用:
Java深度歷險(二)——Java類的加載、鏈接和初始化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容