第7章 虛擬機類加載機制

第7章 虛擬機類加載機制

1、概述

​ 虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。java

​ Java中語言裏,類型的加載,鏈接和初始化都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增長一些性能開銷,可是會爲Java應用程序提供高度的靈活性,Java裏天生可動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特色實現的.程序員

2、類加載的時機

​ 類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱爲鏈接(Linking)、這7個階段的發生順序如圖所示。數組

​ 加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化以後再循序漸進地開始,這是爲了支持Java語言的運行時綁定。安全

​ 什麼狀況下須要開始類加載過程的第一個階段?Java虛擬機規範並無進行強制約束,但對於初始化階段,虛擬機規範則嚴格規定了有且只有5種狀況必須對類進行「初始化」(加載、驗證、準備天然須要在此以前開始):數據結構

​ 1)遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行初始化,則須要先觸發其初始化。生成這4條指令的最多見Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段的時候,以及調用一個類的靜態方法的時候。佈局

​ 2)使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。性能

​ 3)當初始化一個類的時候,若是其父類尚未進行過初始化,則須要先觸發其父類的初始化。spa

​ 4)當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類。線程

​ 5)當使用JDK1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄對應的類沒有初始化,則須要先觸發其初始化。翻譯

​ 這五種場景的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。

3、類加載的過程

1.加載

​ 「加載」是「類加載」過程的一個階段,在加載階段,虛擬機須要完成如下3件事情:

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

​ 對於數組類而言,狀況與非數組類有所不一樣,數組類自己不經過類加載器建立,它是由java虛擬機直接建立的。但數組類與類加載器仍然有很密切的關係,由於數組類元素類型最終是要靠類加載器去建立,一個數組類建立過程遵循如下規則:

  • 若是數組的組件類型是引用類型,那就遞歸採用加載過程加載這個組件類型,數組將在加載該組件類型的類加載器的類名稱空間上被標識(一個類必須與類加載器一塊兒肯定惟一性)
  • 若是數組的組件類型不是引用類型(int[]數組),Java虛擬機會把數組標記爲與引導類加載器關聯。
  • 數組類的可見性與它的組件類型的可見性一致,若是組件類型不是引用類型,那數組類的可見性將默認爲public。

​ 加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所須要的格式存儲在方法區之中,方法區的數據存儲格式由虛擬機實現自行定義。而後在內存中實例化一個java.lang.Class對象(HotSpot虛擬機中,Class對象比較特殊,雖然是對象,可是存放在方法區中),這個對象將做爲程序訪問方法區中這些類型數據的外部接口。

​ 加載階段與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。

2.驗證

​ 驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

​ Java語言自己是相對安全的語言,使用純粹的Java代碼沒法作到諸如訪問數組邊界意外的數據、將一個對象轉型爲它並未實現的類型、跳轉到不存在的代碼行之類的事情,若是這樣作了,編譯器將拒絕編譯。Class文件並不必定非要由Java源碼編譯而來,可使用任何途徑產生,在字節碼語言層面上,上述java代碼沒法作到的事情都是能夠實現的。虛擬機若是不檢查輸入的字節流,對其徹底信任的話,極可能會由於載入了有害的字節流而致使系統的崩潰,因此驗證是虛擬機對自身保護的一項重要工做。

​ 驗證階段是否嚴謹,決定了java虛擬機是否可以承受惡意代碼的攻擊,從執行性能的角度上講,驗證階段的工做量在虛擬機的類加載子系統中又佔了至關大的一部分。總體上看,驗證階段大體會完成4個階段的檢驗動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

3.準備

​ 準備階段是正式爲變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念,這時候進行內存分配的僅包括類變量(static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值

​ 一些「特殊狀況」:若是類字段的字段屬性表存在ConstantValue屬性,那在準備階段變量value就會被初始化爲ConstantValue所指定的值,假設類變量定義爲public static final int value = 123;

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

4.解析

​ 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

  • 符號引用(Symbolic References):以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義的定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。各類虛擬機實現的內存佈局能夠不相同,可是它們接受的符號引用必須都是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。
  • 直接引用(Direct References):能夠直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不相同,若是有直接引用,那引用的目標一定已經在內存中存在。

5.初始化

​ 類初始化階段是類加載過程的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器以外,其他徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

​ 在準備階段,變量已經賦過一次初始值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源。

4、類加載器

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

1.類與類加載器

​ 類加載器雖然只用於實現類的加載動做,但它在Java程序中起到的做用卻遠遠不限於類加載階段。對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器都擁有一個獨立的類名稱空間。比較兩個類是否「相等」,只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。

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

2.雙親委派模型

​ 從Java虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器由C++語言實現,是虛擬機的一部分;另外一種就是全部其餘的類加載器,這些類加載器都是由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader。

​ 從Java開發人員的角度來看,類加載器還能夠劃分的更細緻:

  • 啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在 \lib目錄中的,或者被-Xbootclasspath參數所指定路徑中的被虛擬機識別的類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給引導類加載器,直接使用null代替便可。
  • 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載 \lib\ext目錄中的,或者被java.ext.dirs系統變量指定的路徑中的類庫,開發者能夠直接使用擴展類加載器
  • 應用程序類加載器(Application ClassLoader):這個類由sun.misc.Launcher$AppClassLoader實現.因爲這個類加載器是ClassLoader中的getSystemClassLoader方法的返回值,因此通常也稱爲系統類加載器,它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過類加載器,通常狀況下這個就是程序中默認的類加載器。

​ 上圖中展現的這種類加載器之間的層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器,這裏類加載器的父子關係通常不會以繼承(Inheritace)的關係來實現,而是都是用組合(Composition)關係來複用父加載器的代碼。

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

​ 使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層器關係,例如Object類它存放在rt.jar之中,不管哪一個類加載器要加載這個類,最終都是委派給處於模型頂端的啓動類加載器進行加載,所以Object類在程序各個類加載器環境中都是同一個類,相反,若是沒有雙親委派模型,由各個類加載器自行加載的話,若是用戶本身編寫了一個Object類並放在classpath中,那系統中將出現多個不一樣的Object類,應用程序也會變得混亂。

​ 雙親委派模型對於保證Java程序的穩定運行很重要但實現卻很是簡單,先檢查是否已經被加載過,若是沒有就調用父加載器的loadClass方法,若父加載器爲空則默認使用啓動類加載器做爲父加載器,若是父加載器加載失敗,拋出ClassNotFoundException後,在調用本身的loadClass方法進行加載。

3.破壞雙親委派模型

​ 線程上下文類加載器(Thread Context ClassLoader)。這個類加載器能夠經過java.lang.Thread類的setContextClassLoader方法進行設置,若是建立線程時還未設置,他將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置過,那這個類加載器默認就是應用程序類加載器。

​ 有了線程上下文類加載器,就能夠父類加載器請求子類加載器完成類加載動做,這種行爲就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的通常性原則。

相關文章
相關標籤/搜索