類加載機制

類的生命週期

  一個java文件的整個生命週期,總共要經歷加載-驗證-準備-解析-初始化-使用-卸載這幾個階段,有的人把驗證準備解析概括爲一個階段稱爲連接,全部有的說5個階段的,也有說7個階段的,兩種說法。java

何時開始加載?

  1.用new實例化對象的時候。程序員

  2.讀取或者設置一個類的靜態字段的時候。數據庫

  3.調用一個類的靜態方法的時候。緩存

  4.使用java.lang.reflect包的方法對類進行反射的時候,若是類沒有進行過初始化,則須要先觸發其初始化。安全

  5.當初始化一個類的時候,若是發現這個類的父類尚未進行過初始化,則須要先觸發其父類的初始化。數據結構

  6.當虛擬機啓動的時候,若是java程序中包含main()主函數的類,則該類的加載由JVM自動觸發。框架

加載

反射機制的原理

  所謂加載,就是將java類的字節碼文件加載到機器內存中,並在內存中構建出java類的原型-類模板對象。ide

  所謂類模板對象,其實就是java類在JVM內存中的一個快照,JVM將從字節碼文件中解析出常量池、類字段、類方法等信息存儲到類模板中,這樣JVM在運行期便能經過類模板而獲取Java類中的任意信息,可以對java類的成員變量進行遍歷,也能進行java方法的調用,這就是反射機制背後的原理,若是JVM沒有將java類的聲明信息保存起來,則JVM在運行期也沒法對類進行反射。函數

要完成的三件事

  在這個加載階段,虛擬機須要完成如下三件事情:spa

  1.經過一個類的全限定名(完整包名、URL地址、數據庫生成、等等)來獲取定義此類的二進制字節流。

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

  3.在內存中生成一個表明整個類的java.lang.Class對象,做爲方法區整個類的各類數據的訪問入口。

加載階段完成以後,虛擬機外部的二進制字節流就按照虛擬機所須要的格式存儲在方法區之中,而後在內存中實例化一個java.lang.Class類的對象。

交叉進行

  須要注意的是,加載階段與後面的驗證準備解析階段並不是是阻塞式進行,可能加載階段還沒有完成,後面的階段就已經開始了。

驗證

驗證的重要性

  驗證這一階段的目的是爲了確保class文件的字節流中包含的信息是否符合虛擬機的要求。這個很好理解,隨便一個程序,你少寫一個標點符號看看還能不能進行編譯。

  虛擬機若是不檢查輸入的字節流,對其徹底信任的話,極可能會由於載入了有害的字節流而致使系統崩潰,因此驗證是虛擬機對自身保護的一項重要操做。

文件格式驗證

  首先須要驗證字節流是否符合class文件格式的規範,而且能夠被當前虛擬機處理。

驗證內容列舉幾項:

  1.版本號是否在當前虛擬機處理範圍以內。

  2.常量池的唱兩種是否有不被支持的常量類型。

  3.指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。

  4.class文件中各個部分以及文件自己是否有被刪除的或附加的其餘信息。

元數據驗證

  接下來是對字節碼描述的信息進行語義分析,以保證符合java語言規範的要求。

驗證內容列舉幾項:

  1.這個類有沒有父類,由於除了java.lang.Object以外,全部的類都應該有父類 。

  2.這個類的父類是否繼承了不容許被繼承的類(好比被final修飾的類)。

  3.若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法。

  4.類中的字段、方法是否與父類產生矛盾。

字節碼驗證

  這個驗證將對類的方法體進行校驗分析,保證被校驗的方法在運行時不會作出危害虛擬機安全的事件。

驗證內容舉例幾項:

  1.保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做棧放置了一個Int類型的數據,使用時卻按照long類型來加載入本地變量表中。

  2.保證跳轉指令不會跳轉到方法體之外的字節碼指令上。

  3.保證方法體中的類型轉換是有效合法的。

符號引用驗證

  符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗。

驗證內容舉例幾項:

  1.符號引用中經過字符串描述的全限定名是否能找到對應的類。

  2.在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。

  3.符號引用中類、字段、方法的訪問性(private/protected/public/default)是否可被當前類訪問。

準備

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

  這個階段有兩個概念容易產生混淆。首先,準備階段進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。

  其次,這裏所說的初始值一般狀況下是數據類型的零值,假設一個類變量定義爲:

public static int value = 123;

  那變量value在準備階段事後的初始值爲0,而不是123,由於這時候尚未開始執行任何java方法。

  可是,若是是final修飾的變量,如:

public static final int value = 123;

  那麼在準備階段變量value就會被初始化爲123。

解析

  解析的過程就是JVM將常量池中的符號引用替換爲直接引用的過程。

  好比說一個變量的類型是某個對象,那麼解析的時候須要把這個變量類型替換成直接指向該對象的指針。

  對同一個符號引用進行屢次解析是很常見的事情,虛擬機會對第一次解析的結果進行緩存,從而避免解析動做重複執行。

初始化

  完成上面幾個階段後,便會進入類的初始化階段。

  所謂初始化,說白了就是調用java類的<clinit>()方法,該方法是編譯器在編譯期間自動生成的,當java類中出現靜態字段或者包含static{}塊時,編譯出來的java字節碼文件中就會自動包含一個名爲<clinit>的方法,該方法不能由程序員在java程序中調用,只能由JVM在運行期調用,這個調用的過程就是java類的初始化。

注意:<clinit>()方法並不是類的構造函數。

類加載器

  要想再JVM內部建立一個與java類徹底對等的結構模型,必須通過類加載器。

類加載器的定義

  java體系中定義了3種類加載器,分別以下:

 

  1.Bootstrap ClassLoader,引導類加載器,也被稱做啓動類加載器,加載指定的JDK核心類庫,該加載器是由C++語言定義的,是虛擬機自身的一部分,沒法由java應用程序直接引用,負責加載下列三種狀況下所指定的核心類庫:

①、%JAVA_HOME%/jre/lib目錄
②、-Xbootclasspath參數所指定的目錄
③、系統屬性sun.boot.class.path指定的目錄中特定名稱的jar包

 

  2.Extension ClassLoader,擴展類加載器,加載擴展類,擴展JVM的類庫,該加載器加載下列兩種狀況下所指定的類庫:

①、%JAVA_HOME%/jre/lib/ext目錄
②、系統屬性java.ext.dirs所指定的目錄中的全部類庫

 

  3.App ClassLoader,系統類加載器,也被稱做應用程序類加載器,加載java應用程序類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

 

類與類是否相等

  對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立在java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。

  這句話反過來講就是,比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下,比較纔有意義,不然,就算這兩個類都來源於同一個class文件,被同一個虛擬機加載,只要加載他們的類加載器不一樣,那麼這兩個類就一定不相等。

  這裏所指的「相等」,包括equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字對所屬關係斷定狀況。

  如下演示不一樣類加載器加載出的類比較結果:

public class Test {
    public static void main(String[] args) throws Exception{
        ClassLoader loader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try{
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null){
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                }catch (IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj1 = new Test();
        System.out.println("obj1:" + obj1.toString());
        Object obj2 = loader.loadClass("Test").newInstance();
        System.out.println("obj2:" + obj2.toString());
        System.out.println(obj1.equals(obj2));
    }
}

輸出結果:

obj1:Test@88ff2c
obj2:Test@c0663d
false

  該示例中,obj1對象是由系統類加載器加載的,obj2對象是由咱們自定義的類加載器加載的,雖然都來自於同一個class文件,但依然是兩個獨立的類。

 

  這裏有的人會遇到一個問題,把obj2轉成Test類型,以下:

Test obj2 = (Test)loader.loadClass("Test").newInstance();

  一運行會發現拋出這樣一個錯:

Exception in thread "main" java.lang.ClassCastException: Test cannot be cast to Test

  緣由在於等號左邊所聲明的Test類型並無明確爲其指定類加載器,因此JVM會使用系統類加載器加載Test類,而等號右邊則明確使用了自定義的類加載器加載Test類,因此等號左右兩邊的兩個Test類型的加載器並非同一個。

  這種異常在使用第三方框架好比Spring的時候會比較常見,究其緣由,是由於不少中間件內部都有自定義的類加載器,所以被內存加載器所加載的類型,是沒有辦法直接轉換爲使用默認加載器加載的類型。

雙親委派模型

  JVM加載一個類的邏輯爲如下三步:

第一步:在當前加載器的緩存中查找有沒有這個類,若是有,直接返回,不然走下一步。

第二步:跳到父加載器,重複第一步內容,直到跳到最頂級的引導類加載器爲止,若是緩存中尚未這個類,則繼續下一步。

第三步:引導類加載器進行加載,若是加載不到,則讓子加載器一級一級進行加載,直到加載成功。

 

  假設當前加載的是java.lang.Object這個類,當JVM準備加載時,JVM默認會使用系統類加載器去加載,按照上面三步的邏輯,第一步走過,由於系統類加載器和擴展類加載器的緩存中都不會有該類,走到第二步 到了引導類加載器,若是加載過,則取緩存,若是沒加載過,則由引導類加載器進行加載,若是引導類加載器的搜索範圍內找不到該類,那麼會下發到擴展類加載器進行加載。

  這就是雙親委派機制。

  這種機制保證核心類庫必定是由引導類加載器進行加載,而不會被多種加載器加載,不然每一個加載器都會加載一遍核心類庫,這個世界就亂了,同時也會存在安全隱患。

 

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

如圖:

 

  簡而言之,雙親委派從本質上而言,其實規定了類加載的順序是:引導類加載器先加載,若加載不到,由擴展類加載器加載,若還加載不到,纔會由系統類加載器或自定義的類加載器進行加載。

"new"作了什麼事情?

  類的生命週期分爲7個階段,加載完成以後須要進行連接(驗證、準備、解析)和初始化,在連接階段,字節碼指令會被重寫,將其所引用的常量池的索引號轉換爲直接引用。

  好比說,在實例化一個類的時候,編譯後生成的字節碼指令爲:new #2。後面這個#2表示常量池中索引爲2的元素,該元素指向某個java類的全限定名。

  若是是實例化Long,常量池中2號索引裏存在的是字符串:java.lang.Long。重寫後的new字節碼指令,後面跟着的就不是#2了,就是指向"java.lang.Long"這個字符串的內存地址。

  當JVM真正運行到new這條指令的時候,它要根據java類的全限定名稱,在內存metaspace區定位到這個java類在內存中的類模板對象-instanceKlass。類模板對象包含了原始java類中的一切信息,JVM會根據這個模板建立出java類的實例對象。

注意:爲了不每次new都要進行一次定位,JVM會在第一次執行new指令時,就會將定位到的類模板對象緩存起來,這樣子後續須要再次實例化一樣的java類對象時,便會直接從緩存中讀取模板。

相關文章
相關標籤/搜索