Jvm類的加載機制

1.概述

虛擬機加載Class文件(二進制字節流)到內存,並對數據進行校驗、轉換解析和初始化,最終造成可被虛擬機直接使用的Java類型,這一系列過程就是類的加載機制。html

2.類的加載時機

類從被虛擬機加載到內存開始,直到卸載出內存爲止,整個生命週期包括:加載——驗證——準備——解析——初始化——使用——卸載 這7個階段。其中驗證、準備、解析3個部分統稱爲鏈接。java

生命週期圖以下:程序員

  其中加載、驗證、準備、初始化、卸載這5個階段順序是肯定的,類的加載過程必須按照這種順序進行開始,而解析階段則不必定:它在某種狀況下能夠在初始化以後再開始,這也是爲了支持Java語言的動態綁定。數組

  哪些狀況能觸發類的初始化階段?(前提:加載、驗證、準備天然是已經執行完了)

  1. 遇到new、getstatic、putstatic、invokestatic 這4條指令時若是類沒有初始化則會觸發其初始化,(工做中觸發這4種指令最多見的場景:new實例化對象、讀取or設置類的靜態字段【final修飾或者已經把靜態字段放入常量池的除外】、調用類的靜態方法)
  2. 使用反射的時候
  3. 初始化類的時候若是其父類還沒進行初始化,則須要先觸發父類的初始化
  4. 虛擬機啓動時,須要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個類
  5. 使用jdk1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄。切這個句柄對應的類沒有初始化,則須要先觸發其初始化

   注意:全部引用類的方式都不會觸發初始化(被動引用)例如:建立數組、引用final修飾的變量、子類引用父類的靜態變量 不會觸發子類初始化可是會觸發父類初始化安全

3.類的加載過程

加載

 加載是類加載的一個階段,在加載階段  虛擬機須要完成下面3件事情數據結構

  1. 經過類的全限定名來獲取定義此類的二進制字節流 
  2. 將這個字節流所表明的靜態存儲結構轉化成方法區的運行時數據結構
  3. 在內存中生成一個表明此類的java.lang.Object對象,做爲方法區這個類的各類數據的訪問入口

 相對於類加載的其餘階段,加載階段(準確的說,是加載階段中獲取類的二進制字節流的動做)是開發人員可控性最強的。由於加載階段既可使用系統提供的引導類加載器來完成,也能夠由開發人員自定義的類加載器來完成(即重寫類加載器的loadClass()方法)。多線程

 加載完成後,外部的二進制字節流就轉化成虛擬機所需的格式存儲在方法區中,而後在內存中實例化一個java.lang.Class類的對象。這個對象將做爲程序訪問方法區中的這些類型數據的外部接口。源碼分析

 加載階段與鏈接階段的部份內容是交叉進行的,並非加載完成後才能執行驗證等操做。這些夾在加載之中的動做仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。this

- 驗證

 驗證是鏈接的第一步,爲了保證加載的二進制字節流所包含的信息是符合虛擬機規範的。spa

 驗證階段大體分爲下面4個檢驗動做:

  文件格式驗證:驗證字節流是否符合Class文件格式規範。例如:是否以魔數 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機處理範圍內、常量池中的常量是否有不被支持的類型······。

  元數據驗證:對字節碼描述的信息進行語義分析。例如: 這個類是否有父類、是否正確的繼承了父類。

  字節碼驗證:經過數據流和控制流的分析,肯定程序語義是合法的、符合邏輯的(說白了就是對類的方法體進行分析確保方法在運行時不會危害虛擬機)。

  符號引用驗證:確保解析動做能正常執行。

驗證階段是很是重要,但不必定是必要的階段(由於對程序運行期沒有影響)。若是所運行的所有代碼都已經被反覆使用和驗證過,那麼在實施階段可使用-Xverify:none參數來關閉驗證。

- 準備

正式爲類變量分配內存並設置類變量初始值。這些變量所使用的內存都將在方法區中進行分配。

注意:

  • 此時被分配的僅僅是靜態變量,而不是實例變量,實例變量將隨着對象實例一塊兒分配在Java堆中
  • 初始值一般狀況下是數據類型的零值。假如定義一個靜態變量 public static int value = 123;那麼value在準備階段初始值爲0而不是123。
  • 被final修飾的變量在準備階段就初始化爲屬性所指定的值。例如: public static final int value = 123;那麼value在準備階段初始值就是123。

- 解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

符號引用:以一組符號來描述引用的目標,符號能夠是任何形式的字面量。

直接引用:指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。

- 初始化

初始化階段是執行類構造器<clinit>()方法的過程。在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員制定的參數值去初始化類變量和其餘資源。

類構造器<clinit>()方法:是由編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併產生的。

編譯器收集的順序是由語句在源文件中出現的順序決定的;靜態代碼塊只能訪問定義在靜態塊以前的變量,定義在它以後的變量,在前面的靜態塊中能夠賦值,但不能訪問。

非法向前引用示例

public class SuperClass {
    public static int va;
    static {
        value = 1;            //能夠編譯經過
        va = value;           //報錯  非法向前引用
        System.out.println("父類初始化");
    }

    public static int value = 123;
}

 <clinit>()方法 對類或接口來講並非必須的,若是一個類中沒有靜態代碼塊,也沒用對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>方法 

接口中不能使用靜態塊,但仍能夠有變量賦值操做,所以接口和類同樣都會生成<clinit>方法。不一樣的是,接口初始化不須要先執行父類的初始化,只有當父接口中的變量使用時,纔會觸發父接口的初始化。另外接口的實現類也不會觸發接口的實例化。

虛擬機會保證一個類的<clinit>()方法在多線程中被正確的加鎖、同步,若是多個線程去初始化一個類,那麼只會有一個線程去執行類的<clinit>()方法,其餘線程都處於等待狀態。只能活動線程執行完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。

 4.類加載器

   虛擬機設計團隊把類加載中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼塊稱爲類加載器。

從Java開發人員的角度看,類加載器大體分爲以下3種

  啓動類加載器(Bootstrap Classloader):負責將存放在<JAVA_HOME>\lib(Javahome即jdk的安裝目錄)目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib下面也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接使用

  擴展類加載器(Extension Classloader):該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的系統路徑中的全部類庫。開發者能夠直接使用擴展類加載器。

  應用程序類加載器(Application Classloader):該加載器由sun.misc.Launcher$AppClassLoader實現,它負責加載用戶類路徑(ClassPath)上所指定的類庫。開發者能夠直接使用此加載器。若是應用程序中沒有自定義的類加載器,那麼這個就是程序默認執行的類加載器。(系統加載器)

咱們的應用程序都是由這3種類加載器相互配合進行加載的。若是有必要,還能夠加入自定義的類加載器。

這些類加載器之間的關係以下圖:

 

5.雙親委派模型: 

  雙親委派模型的工做過程是:若是一個類加載器收到了一個類加載請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一層的加載器都是如此,所以全部的加載請求最終都應該到達頂層的啓動類加載器。只有當父加載沒法完成這個加載請求時,子加載器纔會嘗試本身去加載。

雙親委派機制:

一、當ApplicationClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

二、當ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

三、若是BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;

四、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。

ClassLoader源碼分析:    

 

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 先檢查此類是否已被加載
            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;
        }
    }                                         

雙親委派模型意義:

  • 系統類防止內存中出現多份一樣的字節碼
  • 保證Java程序安全穩定運行

參考

《深刻理解Java虛擬機》

https://www.cnblogs.com/ityouknow/p/5603287.html 

相關文章
相關標籤/搜索