JVM-類加載

上文講到一個.java文件是如何變成一個.class文件以及Class文件的組成,在Class文件中描述的各類信息,最終都需要加載到虛擬機中之後才能被運行和使用。那麼一個.class文件是如何加載到虛擬機中使用的呢?它是通過類加載器通過類加載的過程實現的。一個類的加載過程分為加載、驗證、準備、解析、初始化、使用、銷毀,JVM通過類加載器實現完成加載這一步驟,類加載器又分為BootStrapClassLorder、ExtensionClassLoader、ApplicationClassLoader、自定義類加載器。

一、類加載

一個類型從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期將會經歷加載、驗證、準備、解析、初始化、使用、卸載七個階段,其中驗證、準備、解析三個部分統稱為連接。

需要注意的是加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類型的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定特性(也稱為動態綁定或晚期綁定)。

何時會進行類加載

1、遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段(而加載、驗證、準備自然需要在此之前開始)
new:創建類實例 使用new關鍵字實例化對象的時候
getstatic、putstatic訪問類的域和類實例域 讀取或設置一個類型的靜態字段
invokestatic 調用命名類中的靜態方法
2、使用java.lang.reflect包的方法對類型進行反射調用的時候,如果類型沒有進行過初始化,則需要先觸發其初始化。
3、當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4、當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
… 等等
接口的加載過程與類加載過程稍有不同 編譯器仍然會為接口生成「()」類構造器,用於初始化接口中所定義的成員變量 但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。

1.加載:

加載,是指查找位元組流,並且據此創建類或者接口的過程。加載階段既可以使用Java虛擬機里內置的引導類加載器來完成,也可以由用戶自定義的類加載器去完成,開發人員通過定義自己的類加載器去控制位元組流的獲取方式(重寫一個類加載器的findClass()或loadClass()方法),實現根據自己的想法來賦予應用程序獲取運行代碼的動態性。
但是對於數組類而言,情況就有所不同,數組類本身不通過類加載器創建,它是由Java虛擬機直接在內存中動態構造出來的,因為數組它並沒有對應的位元組流,由Java虛擬機直接生成的。

在加載階段,Java虛擬機需要完成以下三件事情
1、通過一個類的全限定名來獲取定義此類的二進制位元組流。這裡不一定是class文件 可以從ZIP壓縮包、JAR、EAR、WAR等格式中讀取,可以從網絡中獲取,也可以運行時獲取,也就是動態代理技術。
2、將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
3、在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。

加載一個類的時候會去先加載其父類,而且會有懶加載的機制(就是用到的時候才去加載這個類)。

public class Test_2 {
    public static void main(String[] args) {
        System.out.println(B.a);
    }
}
class A{
    public static  String a = "str";

    static {
        System.out.println("AAAAAA");
    }
}
class B extends A{
    static {
        a+="aaa";
        System.out.println("BBBBB");
    }
}

輸出 AAAAAA str

為什麼B類沒有被加載?

因為JVM會先判斷是否加載,才會有初始化的動作。
JVM又是懶加載 只有用到的時候才會去加載,所以JVM判斷只要加載A就可以了,B的內部沒有任何東西被使用,所以B並沒有加載。
JVM加載類是採用懶加載,用到的時候再去加載,一些根類是採用預加載,一開始就會加載進虛擬機里,比如String Interge常用的。

2.驗證:

驗證階段大致上會完成下面四個階段的檢驗動作:文件格式驗證、元數據驗證、位元組碼驗證和符號引用驗證。
1、文件格式驗證:包含驗證是否以魔數0xCAFEBABE開頭。主、次版本號是否在當前Java虛擬機接受範圍之內等信息的驗證元數據驗證:這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)等信息的驗證 側重點是驗證描述的信息符合《Java語言規範》的要求
2、位元組碼驗證:對類的方法體(Class文件中的Code屬性)進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為,
3、符號引用驗證:最後一個階段的校驗行為發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。符號引用中通過字符串描述的全限定名是否能找到對應的類等信息的驗證
需要注意的是驗證階段對於虛擬機的類加載機制來說,是一個非常重要的、但卻不是必須要執行的階段 可以使用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

3.準備:

準備階段是正式為類中定義的變量(即靜態變量,被static修飾的變量)分配內存並設置類變量初始值的階段
需要注意的是這時候進行內存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中
public static int value = 123; 執行完之後變成了 value = 0;
把value賦值為123的動作要到類的初始化階段才會被執行
public static final int value = 123; 準備階段執行完之後變成了 value = 123;
如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量值就會被初始化為ConstantValue屬性所指定的初始值,

總結:
1、final static修飾的在準備階段直接分配內存並賦值了
2、static修飾的是在準備階段進行分配內存會初始化賦一個默認值 初始化階段的()進行初始化賦值的
3、非靜態的是在初始化階段的中分配內存並賦值的

4.解析:

解析階段是Java虛擬機將常量池內的符號引用替換為直接引用的過程。java文件通過編譯之後會變成符號引用
類似這樣的 #7.#28 這樣的為符號引用 在16進制文件里就是 0A 00 07 00 1C
解析階段就是把#7這樣的符號引用變成直接引用

1.符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義定位到目標即可。
你比如說某個方法的符號引用,

如:「java/io/PrintStream.println:(Ljava/lang/String;)V」。裏面有類的信息,方法名,方法參數等信息。
 #1=Methodref   #9.#33 這樣的為符號引用

2.直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。
0x123123123123 這樣的地址為直接引用(不指向常量池 而是直接指向內存地址)

為什麼會有符號引用?

因為在類沒有加載的時候,也不能確保其調用的資源被加載,更何況還有可能調用自身的方法或者字段,就算能確保,其調用的資源也不會每次在程序啟動時,都加載在同一個地址。簡而言之,在編譯階段,位元組碼文件根本不知道這些資源在哪,所以根本沒辦法使用直接引用,於是只能使用符號引用代替,而類加載過程中的解析也只是解析一部分,只對類加載時可以確定的符號引用進行解析。比如父類、接口、靜態字段、調用的靜態方法等(靜態鏈接)。還有一部分,比如方法中的局部變量、實例字段等在程序運行期間完成的;也就是使用前才去解析它(動態鏈接)。

《Java虛擬機規範》之中並未規定解析階段發生的具體時間,虛擬機實現可以根據需要來自行判斷,到底是在類被加載器加載時就對常量池中的符號引用進行解析(靜態鏈接),還是等到一個符號引用將要被使用前才去解析它(動態鏈接)。對同一個符號引用進行多次解析請求是很常見的事情,除invokedynamic指令以外,虛擬機實現可以對第一次解析的結果進行緩存。因為invokedynamic指令的目的本來就是用於動態語言支持,這裡「動態」的含義是指必須等到程序實際運行到這條指令時,解析動作才能進行。相對地,其餘可觸發解析的指令都是「靜態」的,可以在剛剛完成加載階段,還沒有開始執行代碼時就提前進行解析。Java有了Lambda表達式和接口的默認方法,它們在底層調用時就會用到invokedynamic指令.

5.初始化:

除了在加載階段用戶應用程序可以通過自定義類加載器的方式局部參與外,其餘動作都完全由Java虛擬機來主導控制。直到初始化階段,Java虛擬機才真正開始執行類中編寫的Java程序代碼,將主導權移交給應用程序。

進行準備階段時,變量已經賦過一次系統要求的初始零值,而在初始化階段,則會根據程序員通過程序編碼制定的主觀計划去初始化類變量和其他資源。初始化階段就是執行類構造器clinit()方法的過程。clinit()並不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動生成物。
1、clinit()方法是由編譯器自動收集類中的所有類變量(靜態變量)的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的clinit()方法與類的構造函數(即在虛擬機視角中的實例構造器init()方法)不同,它不需要顯式地調用父類構造器,Java虛擬機會保證在子類的clinit()方法執行前,父類的clinit()方法已經執行完畢。clinit()方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成()方法。
2、init()對象構造時用以初始化對象的,構造器以及非靜態初始化塊中的代碼。

二、類加載器

  • BootStrapClassLorder 根類(啟動類)加載器: 它用來加載 Java 的核心類,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader(負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類。
  • ExtensionClassLoader 擴展類加載器:它負責加載JRE的擴展目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類。由Java語言實現,父類加載器為null。
  • ApplicationClassLoader 系統類(應用程序類)加載器:如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作為父加載器。由Java語言實現,父類加載器為ExtClassLoader。
  • 自定義類加載器 自定義類加載器只需要繼承java.lang.ClassLoader類 用於加載自己定義目錄下的類 其父類加載器默認為ApplicationClassLoader。
boot 類加載器是加載 jre/lib
ext  類加載器是加載 jre/ext/lib
app  類加載器是加載 classpath
classpath是我們程序執行時候打印的目錄 classpath 也就是我們程序員自己寫的代碼

/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55758:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 
-classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/dnsns.jar: 
...

Main函數所在的類是用什麼類加載器加載的?
    App
    jvm要去加載main函數所在的類
    boot -> Ext -> App

1. 類加載器初始化過程:

  • C++調用java代碼創建JVM啟動器 實例sun.misc.Launcher 該類由引導類加載器負責加載其它類加載器 sun.misc.Launcher.getLauncher()
  • Launcher.getLauncher()方法里做的事情就是初始化ExtensionClassLoader和ApplicationClassLoader 並把他們的關係構造好 ApplicationClassLoader的父類加載器是ExtensionClassLoader
  • 父類加載器要注意 不是繼承關係 只是父類加載器 他們繼承的類都是ClassLoader
    ExtClassLoader AppClassLoader 都是Launcher類里的內部類 他們都是繼承URLClassLoader(最終繼承的都是ClassLoader)
public class Launcher {
    ...
    static class ExtClassLoader extends URLClassLoader {...}
    static class AppClassLoader extends URLClassLoader {...}
    ...
}

public class URLClassLoader extends SecureClassLoader implements Closeable  {...}
public class SecureClassLoader extends ClassLoader  {...}

// 查看類加載器和父加載器
public class Test_14 {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(DESKeyFactory.class.getClassLoader());
        System.out.println(Test_14.class.getClassLoader());

        System.out.println("*************************");
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassLoader = appClassLoader.getParent();
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println("appClassLoader父類加載器是:" + extClassLoader);
        System.out.println("extClassLoader父類加載器是:" + bootStrapClassLoader);
    }
}

null   // String的類加載器是引導類加載器 打印null說明了它不是Java類實現的 是C++實現的 所以獲取不到
sun.misc.Launcher$ExtClassLoader@eed1f14
sun.misc.Launcher$AppClassLoader@14dad5dc
*************************
appClassLoader父類加載器是:sun.misc.Launcher$ExtClassLoader@eed1f14
extClassLoader父類加載器是:null

2.不同的類加載器加載一個類不相等是為什麼?

類加載器加載的類的存儲文件空間不一樣 boot類加載器有一塊內存 Ext類加載器也有一塊內存 App加載器也有一塊內存 自定義的類加載器也有一塊內存

3.什麼是雙親委派(就是向上委派)

參考://mp.weixin.qq.com/s/E5ZwfpOLqGRK3ZtcsaXAuw

雙親委派機制,其工作原理的是,如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,
請求最終將到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式,
即每個兒子都很懶,每次有活就丟給父親去干,直到父親說這件事我也幹不了時,兒子自己才想辦法去完成。

// 雙親委派的代碼實現邏輯
classLoader1.loadClass("");
點擊loaderClass進去
 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
}

4.雙親委派的作用?

1、效率:通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次
2、安全:java核心api中定義類型不會被隨意替換,假設通過網絡傳遞一個名為java.lang.Integer的類,通過雙親委託模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。
3、提供了擴展性: 比如加密 class文件可以反編譯 不安全 我們可以對class文件進行加密 用自定義的類加載器進行加載

// 證明了一個加載類重複加載一個類只會加載一次
public class Test_3 extends ClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        Test_3 classLoader1 = new Test_3();
        Class<?> class1 = classLoader1.loadClass("com.leetcode.JVM.Test_3");
        System.out.println(class1.hashCode());

        Test_3 classLoader2 = new Test_3();
        Class<?> class2 = classLoader2.loadClass("com.leetcode.JVM.Test_3");
        System.out.println(class2.hashCode());

        System.out.println("*******************************");
        System.out.println(class1==class2);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("ClassLoader");
        return null;
    }
}

791452441
791452441
*******************************
true
System.out.println(class1==class2); 輸出了true為什麼?
HashCode相等 證明了一個加載類重複加載一個類只會加載一次

5.雙親委派的局限性

1、無法做到不委派
2、無法做到向下委派

6.怎麼打破雙向委派

1、自定義加載器去實現 extends ClassLoader 重寫loadClass不委派
2、通過線程上下文類加載器去加載所需的SPI服務代碼 SPI(Service Provider Interface) SPI是通過向下委派打破雙親委派 是一種服務發現機制。它通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件里所定義的類。這一機製為很多框架擴展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機制

public class User {
    public void sout() {
        System.out.println("____User");
    }
}
// 在E盤創建一層文件目錄對應com.leetcode.JVM
// 把User.class文件放到創建的目錄文件夾里
public class Test_15 extends ClassLoader {
    public static void main(String[] args) throws Exception {
        Test_15 test = new Test_15("E:/log");
        Class clazz = test.loadClass("com.leetcode.JVM.User", false);
        Object object = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(object, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
    private String classPath;
    
    Test_15(String name) {
        this.classPath = name;
    }

    private byte[] lodeByte(String name) throws Exception {
        name = name.replaceAll("\\.","/");
        FileInputStream file = new FileInputStream(classPath + "/" + name + ".class");
        int available = file.available();
        byte[] data = new byte[available];
        file.read(data);
        file.close();
        return data;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = lodeByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    @Override
    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();

                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 簡單處理 只有這個路徑下的才使用自己的類加載器
                // 不然無法加載父類Object類 會報錯
                // java類中的核心包都是不允許使用自己的類加載器去加載的(java.lang包下的) 因為沙箱安全機制
                if (!name.startsWith("com.leetcode.JVM")) {
                  c = this.getParent().loadClass(name);
                } else {
                  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;
        }
    }
}

7.Tomcat和com.mysql.jdbc.Driver打破雙親委派機制

1、Tomcat中使用自定義的類加載器去加載 不向上委託加載 但公共使用的類還是使用雙親委派。一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的
不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是
獨立的,保證相互隔離
2、mysql.jdbc.Driver使用SPI機制去加載 向下委託加載。 因為在某些情況下父類加載器需要委託子類加載器去加載class文件 以Driver接口為例,由於Driver接口定義在jdk當中的,而其實現由各個數據庫的服務商來提供 DriverManager由啟動類加載器加載,只能記載JAVA_HOME的lib下文件,而其實現是由服務商提供的,由系統類加載器加載,這個時候就需要啟動類加載器來委託子類來加載Driver實現,從而破壞了雙親委派。

8.沙箱安全和全盤負責委託機制

防止打破雙親委派修改系統類保護核心庫類String Interge等,全盤負責委託機制,指的是當一個ClassLoader裝載一個類時,除非顯示的使用另外一個ClassLoader,該類所依賴及引用的類也由這個類的ClassLoader加載。

Tags: