深刻理解Java虛擬機讀書筆記-第7章 虛擬機類加載機制

第7章 虛擬機類加載機制

7.1 概述

類加載機制:從Class文件到內存中Java類型的過程。各個階段時間段上能夠有重疊。 類加載是在運行期間執行的,也描述爲動態加載和動態鏈接。java

7.2 類加載時機

截屏2020-08-24下午1.28.17.png 對於何時開始加載, 《Java虛擬機規範》沒有強制約束。可是嚴格規定了有且只有六種狀況,若是類沒有初始化,必須當即對類進行"初始化" (加載、鏈接必然會先執行),稱之爲主動引用:數據庫

  • 遇到new、getstatic、putstatic、invokestatic字節碼指令時
    • 指令new實例化對象。
    • 指令getstatic/putstatic 訪問其靜態對象(被final修飾,編譯期已放入常量池的除外)。
    • 指令invokestatic,調用其靜態方法。
  • 反射調用
  • 子類初始化時,先觸發父類的初始化
  • 虛擬機啓動時,初始化用戶指定的要執行的主類
  • MethodHandle解析結果爲REF_getStatic,REF_setStatic,REF_invokeStaitc,Ref_newInvokeSpecial
  • 當一個接口定義了JDK 8新加入的默認方法(default修飾)

不會觸發類初始化的幾個場景舉例:數組

  • 經過子類引用父類的靜態字段,不會致使子類初始化
  • 經過數組定義引用類,不會觸發此類的初始化
  • 引用在編譯期已被放入常量池的常量。
//場景一 經過子類引用父類的靜態字段,不會致使子類初始化
public class SuperClass {
    static { System.out.println("SuperClass init!");}
    public static int value=123;
}

public class SubClass extends SuperClass{
    static { System.out.println("SubClass init!");}
}

public class NoInitialization1 {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

複製代碼
//場景二 經過數組定義引用類,不會觸發此類的初始化
public class NoInitialization2 {
    public static void main(String[] args) {
        SuperClass[] scarray=new SuperClass[10];
    }
}
複製代碼
//場景三 引用在編譯期已被放入常量池的常量。
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWRLD="hello world";
}
public class NoInitialization3 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWRLD);
    }
}
複製代碼

對於場景三咱們查看NoInitialization3的字節碼發現,「hello world」已經在其常量池中,使用 ldc指令將常量壓入棧中。而System.out則是使用getstatic指令。這個地方不涉及到ConstClass的初始化安全

Constant pool:
  #4 = String             #25            // hello world
  #25 = Utf8               hello world
{
  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String hello world
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}
複製代碼

7.3類加載過程

7.3.1 加載

加載(loading):從靜態文件到運行時方法區。完成三件事情:markdown

  • 經過一個類的全限定名獲取定義此類的二進制字節流。
    • 能夠從ZIP包中讀取(JAR,WAR等等)
    • 從網絡中獲取,好比Web Applet
    • 運行時計算生成,好比動態代理技術, 「*$Proxy」代理類
    • 數據庫中讀取
    • 加密文件中讀取
    • ......
  • 將該字節流的類靜態存儲結構轉化成方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區中這個類的各類數據的訪問入口。

使用Java虛擬機內置的引導類加載器,或者用戶自定義的類加載器。網絡

7.3.2 驗證

確保字節流符合《Java虛擬機規範》的約束,代碼安全性問題驗證。驗證是重要的,但不是必須的。 四個階段:數據結構

  • 文件格式驗證,此階段經過後,會存儲到方法區。後面階段基於方法區數據進行驗證,再也不讀取字節流。
  • 元數據驗證,類元數據信息語義校驗
  • 字節碼驗證,最複雜,對類的Code部分進行檢驗分析。程序語義合法性,安全性等等
  • 符號引用驗證,在解析過程當中發生,若是沒法經過符號引用驗證,Java虛擬機會拋出java.lang.IncompatibleClassChangeError的子類異常,如 NoSuchFieldError,NoSuchMethodError等等。

7.3.3 準備

一般狀況下,爲類變量(靜態變量),分配內存並設置初始值(零值)。初值並非代碼中賦的值123。123要等到初始化階段。多線程

public static int value = 123;
複製代碼

編譯成class文件app

public static int value;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
...
 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
         0: bipush        123
         2: putstatic     #2                  // Field value:I
         5: return     
...
複製代碼

某些狀況下,設置初始值爲456。好比final修飾的變量。由於變量值456,會提早加入到常量池。ide

public static final int value2 = 456;
複製代碼
public static final int value2;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 456
複製代碼

7.3.4 解析

將常量池內的符號引用替換爲直接引用的過程。 好比說這種,咱們要把 #2替換成實際的類引用,若是是未加載過的類引用,又會涉及到這個類加載過程。

getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
複製代碼
  • 類或接口解析
  • 字段解析
  • 方法解析
  • 接口方法解析

7.3.5 初始化

執行類構造器()方法,非實例構造器()方法 。 ()方法:執行類變量賦值語句和靜態語句塊(static{})。順序爲其在源文件中順序決定。 舉例1:非法向前引用變量。 value的定義在 static{} 以後,只能賦值,不能讀取值。

public class PrepareClass {
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
    public static int value=123;
}
複製代碼

可是下面就能夠

public class PrepareClass {
    public static int value=123;
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
}
複製代碼

class文件參考

0: bipush        123
2: putstatic     #2                  // Field value:I
5: iconst_3
6: putstatic     #2                  // Field value:I
9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
12: getstatic     #2                  // Field value:I
15: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
18: return
複製代碼

舉例2: ()執行順序。子類初始化時,要先初始化父類

public class TestCInitClass2 {

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
複製代碼

輸出:

2
複製代碼

Java虛擬機必須保證()方法在多線程環境下的同步問題。

7.4 類加載器

實現「經過一個類的全限定名來獲取其二進制字節流」的代碼,稱之爲「類加載器」(Class Loader)。

7.4.1 類與類加載器

類與其加載器肯定了這個類在Java虛擬機中的惟一性。

三層類加載器,絕大多數Java程序會用到如下三個系統提供的類加載器進行加載:

  • 啓動類加載器(BootStrap Class Loader)
  • 擴展類加載器(Extension Class Loader)
  • 應用程序類加載器(Application Class Loader)

除了以上三個還有用戶自定義的加載器,經過集成java.lang.ClassLoader類來實現。

啓動類加載器

加載Java的核心庫,native代碼實現,不繼承java.lang.ClassLoader

URL[]  urls= sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url);
}

結果輸出:
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/resources.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jsse.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jce.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/charsets.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jfr.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/classes
複製代碼

擴展類加載器

加載Java的擴展庫,加載ext目錄下的Java類

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

結果輸出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
複製代碼

應用程序類加載器

加載Java應用的類。經過ClassLoader.getSystemClassLoader()來獲取。

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

結果輸出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
...
file:/.../jdk1.8.0_73.jdk/Contents/Home/lib/tools.jar
file:/.../java_sample/out/production/java_sample/  //這是咱們的應用程序
file:/Applications/IntelliJ%20IDEA.app/Contents/lib/idea_rt.jar
複製代碼

自定義類加載器

7.4.2 雙親委派模型截屏2020-08-25下午5.04.25.png

ClassLoader.loadClass

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) {
                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.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
複製代碼

AppClassLoader,ExtClassLoader都繼承URLClassLoader。 URLClassLoader.findClass(name)

protected Class<?> findClass(final String name)throws ClassNotFoundException {
       // 一、安全檢查
    // 二、根據絕對路徑把硬盤上class文件讀入內存
    byte[] raw = getBytes(name); 
    // 三、將二進制數據轉換成class對象
    return defineClass(raw);
    }
複製代碼

若是咱們本身去實現一個類加載器,基本上就是繼承ClassLoader以後重寫findClass方法,且在此方法的最後調包defineClass。 ** 雙親委派確保類的全局惟一性。 例如不管哪一個類加載器須要加載java.lang.Object,都會委託給最頂端的啓動類加載器加載。

參考: 通俗易懂 啓動類加載器、擴展類加載器、應用類加載器 深刻探討 Java 類加載器

7.4.3 線程上下文類加載器

線程上下文類加載器(context class loader),能夠從java.lang.Thread中獲取。 雙親委派模型不能解決Java應用開發中遇到的全部類加載器問題。 例如,Java提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方提供接口實現。常見的SPI有JDBC,JCE,JNDI,JAXP等。SPI接口由核心庫提供,由引導類加載器加載。 而其第三方實現,由應用類加載器實現。此時SPI就找不到具體的實現了。 SPI接口代碼中使用線程上下文類加載器。線程上下文類加載器默認爲應用類加載器。

相關文章
相關標籤/搜索