jvm類加載機制

類加載機制:虛擬機把描述類的數據從class文件加載到類,並對數據進行校驗、轉換解析、初始化,最終形成可以被虛擬機直接使用的java類型。

類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。

類加載過程 其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。 在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。

靜態綁定:編譯時綁定。在程序執行前方法已經被綁定,此時由編譯器或其它連接程序實現。針對java,簡單的可以理解爲程序編譯期的綁定。java當中的方法只有final,static,private和構造方法是前期綁定的。 動態綁定:即運行時綁定。在運行時根據具體對象的類型進行綁定。在java中,幾乎所有的方法都是動態綁定的。

加載

1、通過一個類的全限定名來獲取其定義的二進制字節流。

2、將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。

複習: jvm運行時數據區分爲: 方法區、堆、虛擬機stack、本地方法stack、程序計數器。其中方法區是是每個線程共享的,用於存儲:被虛擬機加載的類信息、常量、靜態變量。

3、在Java堆中生成一個代表這個類的java.lang.Class對象,作爲對方法區中這些數據的訪問入口。

類加載器

說到加載,那jvm是怎麼把這些數據加載到內存的呢?答案是:通過類加載器。java中提供了這麼幾種類加載器:

  • 啓動類加載器:Bootstrap ClassLoader,跟上面相同。它負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是無法被Java程序直接引用的。
  • 擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher 不能識別此Latex公式: ExtClassLoader實現,它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器。
  • 應用程序類加載器:Application ClassLoader,該類加載器由sun.misc.LauncherAppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載.

他們之間存在繼承關係,繼承關係如下圖:

雙親委派模型

具體到代碼裏面:

加載器繼承關係圖

他們是繼承關係,但是實際上是用組合來實現的:

//sun.misc.Launcher
public Launcher() {
        Launcher.ExtClassLoader var1;
        //1. 擴展類加載器
        try {
            //內部引用的是ClassLoader.
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        //2.應用程序加載器
        try {
            //注意這裏傳入參數是上面的var1:擴展類加載器.
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        ...
    }

註釋1出初始化了擴展類加載器,而真正的調用代碼在靜態內部類ExtClassLoader.class中的getExtClassLoader()中,我們接着看。

 static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }
                        //這裏調用靜態類生成.
                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }
        ...
}
public ExtClassLoader(File[] var1) throws IOException {
            //調用父類,第二個參數表示了父加載類是哪個.
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

通過觀察代碼我們發現:ExtClassLoader通過Launcher.ExtClassLoader()靜態方法構造實例,構造時需要依賴父類的構造方法.這裏就實現了它們的加載器父子關係。 AppCalssLoader也類似,有興趣的可以看下。

雙親委派模型

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器纔會嘗試自己去加載該類。

這樣做的好處是什麼?

同一個類的定義:即使兩個類來源於同一個Class文件,只要加載它們的類加載器不同,那這兩個類就必定不相等。這裏的“相等”包括了代表類的Class對象的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對對象所屬關係的判定結果。

Java類隨着它的類加載器(說白了,就是它所在的目錄)一起具備了一種帶有優先級的層次關係。例如,類java.lang.Object類存放在JDK\jre\lib下的rt.jar之中,因此無論是哪個類加載器要加載此類,最終都會委派給啓動類加載器進行加載,這邊保證了Object類在程序中的各種類加載器中都是同一個類。

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文件中的字節流包含的信息符合當前虛擬機的要求。 文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

  • 文件格式的驗證:驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內。經過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。
  • 元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規範的元數據信息。
  • 字節碼驗證:該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行爲。
  • 符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。 這裏所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

public static int value = 3;

那麼變量value在準備階段過後的初始值爲0,而不是3。

public static final int value = 3;

編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲3。

解析

解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程。這就是我們當年學編譯原理的“鏈接”階段。

符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到了內存中。 直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在於內存之中了。

1、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。

2、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束,查找流程如下圖所示:

解析查找流程

class Super{
    public static int m = 11;
    static{
        System.out.println("執行了super類靜態語句塊");
    }
}


class Father extends Super{
    public static int m = 33;
    static{
        System.out.println("執行了父類靜態語句塊");
    }
}

class Child extends Father{
    static{
        System.out.println("執行了子類靜態語句塊");
    }
}

public class StaticTest{
    public static void main(String[] args){
        System.out.println(Child.m);
    }
}

執行結果如下:

執行了super類靜態語句塊
執行了父類靜態語句塊
33

如果註釋掉Father類中對m定義的那一行,則輸出結果如下:

執行了super類靜態語句塊
11

static變量發生在靜態解析階段,也即是初始化之前,此時已經將字段的符號引用轉化爲了內存引用,也便將它與對應的類關聯在了一起,由於在子類中沒有查找到與m相匹配的字段,那麼m便不會與子類關聯在一起,因此並不會觸發子類的初始化。

初始化

初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的Java程序代碼。在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序指定的主觀計劃去初始化類變量和其他資源,或者可以從另一個角度來表達:初始化階段是執行類構造器()方法的過程。

總結

整個類加載過程中,除了在加載階段用戶應用程序可以自定義類加載器參與之外,其餘所有的動作完全由虛擬機主導和控制。到了初始化纔開始執行類中定義的Java程序代碼(亦及字節碼),但這裏的執行代碼只是個開端,它僅限於()方法。類加載過程中主要是將Class文件(準確地講,應該是類的二進制字節流)加載到虛擬機內存中,真正執行字節碼的操作,在加載完成後才真正開始。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章