導讀java
在以前的文章中,咱們經過一張圖的方式(圖:point_up_2:)總體上了解了JVM的結構,並重點講解了JVM的內存結構、內存回收算法及回收器方面的知識。收到了很多讀者朋友們的反饋和指正,在這裏做者向這些提出中肯建議的讀者朋友們表示感謝,謝謝大家的支持。程序員
在今天的文章中將主要和你們一塊兒探討關於類裝載子系統的內容。咱們知道,Java源代碼(.java文件)須要經過編譯器編譯成字節碼文件(.class)後由類裝載子系統(ClassLoader)載入運行時數據區(<jdk1.8以前是載入方法區,>=jdk1.8之後是載入元數據區)才能被後續的Java運行程序(線程)正常使用(實例化或引用)。算法
那麼類裝載的具體機制是什麼樣的呢?下面就讓咱們一塊兒進一步來了解下吧!編程
JVM類裝載概述小程序
與C/C++那些須要在編譯器期進行鏈接工做的語言不一樣,Java類的加載、鏈接和初始化都是在程序運行時完成的,只有在類被須要的時候才進行動態加載,這種方式被稱爲「Java語言的運行期類加載機制」。數組
例如咱們在實際使用Java語言進行編程時一般會編寫一個面向接口的應用程序,能夠等到運行時再指定其實際的實現類。此外,更高級一點的作法是能夠經過自定義的類加載器(關於具體的ClassLoader在後面的內容會提到),讓一個本地的應用程序能夠在運行時從網絡或其餘地方加載一個二進制流做爲程序代碼的一部分(Applet就是這麼幹的),這種組裝應用程序的方式目前已普遍的應用於Java程序之中。安全
類(Class)從被加載到虛擬機內存中開始,到卸載出內存爲止會經歷以下生命週期:網絡
其中驗證、準備、解析3個部分又統稱爲鏈接(Linking)。在以上過程當中,除解析外,加載、驗證、準備、初始化、卸載這5個階段的順序都是肯定的,JVM規定類的加載過程必須按照這種順序循序漸進地開始。而解析階段則不必定,爲了支持Java語言的運行時綁定,解析過程在某些狀況下能夠在初始化階段以後再開始。數據結構
須要注意的是,這些階段並非必須等到上一個階段完成才能開始下一個階段,這些階段一般都是互相交叉地混合式進行的,會在一個階段執行的過程當中就會調用、激活另一個階段。app
聊到這裏,咱們大概瞭解了從一個class字節碼文件變成加載到內存中可以被使用的類,按照前後順序須要通過加載、鏈接、初始化三大主要步驟。鏈接過程又須要經歷驗證、準備、解析三個階段,完成後類被加載至內存,但此時並不能被使用,還須要通過初始化階段。
那麼,在Java中是否全部的類型在類加載的過程當中都須要通過這幾個步驟呢?
咱們知道Java語言的類型能夠分爲兩大類:基本類型、引用類型。基本類型是由虛擬機預先定義好的,因此不會經歷單獨的類加載過程。而引用類型又分爲四種:類、接口、數組類、泛型參數。因爲泛型參數會在編譯的過程當中被擦除(關於類型擦除的知識,你們能夠查下資料),因此在Java中只有類、接口、數組類三種類型須要經歷JVM對其進行鏈接和初始化的過程。
在上述三種類型中數組類是由JVM直接生成的,類和接口則有對應的字節流,字節流最多見的形式就是咱們由編譯器生成的class文件,另外的形式也有在前面說到的經過網絡加載的二進制流(例如網頁中內嵌的小程序 Java applet),這些不一樣形式的字節流都會被JVM加載到內存中,成爲類或接口。
JVM類裝載過程
那麼這些過程具體會幹些什麼事呢?接下來咱們就詳細瞭解下這些具體步驟的細節。
| 加載(Loading)
「加載」是「類加載」(Class Loading)過程的一個階段,是查找字節流並據此建立類的過程。在前面咱們提到過說數組類由於沒有對應的字節流因此是由JVM直接生成的,而對於類和接口來講則須要藉助類加載器(後面會講到)來完成查找字節流的過程。
可是咱們在回答上面關於Java中哪些類型須要經歷類加載的階段時,又明確說了數組類型也是須要JVM對其進行鏈接和初始化的,這是否是有點矛盾呢?事實上,雖然數組類自己並不經過類加載器加載(由虛擬機直接建立),可是數組類與類加載器仍然有很密切的關係,由於數組類的元素類型(Element Type)如對象數組,最終仍是要靠類加載器去建立。
關於數組類的加載建立過程是須要遵循以下規範的:
若是數組的元素類型是引用類型的話,那麼就會遞歸採用前面內容中定義的類加載過程去加載這個元素類型,該數組自己將會在加載該元素類型的類加載器的類名稱空間上被標識(這一點很是重要,在講述類加載器的時候會介紹到,一個類必須與類加載器一塊兒肯定惟一性)。
若是數組的元素類型不是引用類型(例如int[]數組),JVM則會把該int[]數組標記爲啓動類加載器(Bootstrap Classloader)關聯。
實際上,在加載階段虛擬機須要完成如下三件事:
經過一個類的全限定名來獲取定義此類的二進制字節流。
將這個字節流所表明的靜態存儲結構轉換爲方法區(JDK1.8之前)或者元數據(JDK1.8之後)的運行時數據結構。
在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區或元數據區這個類的各類數據的訪問入口。
加載階段完成後,JVM外部的二進制字節流就按照虛擬機所須要的格式存儲在了方法區或元數據區的內存中了。
| 驗證(Verification)
驗證是鏈接階段的第一步,這一階段的主要目的是爲了確保Class文件的字節流中所包含的信息符合當前JVM的要求,而且不會危害JVM自身的安全。虛擬機若是不檢查輸入的字節流,對其徹底信任的話,極可能會由於載入了有害的字節流而致使系統崩潰,因此驗證階段是很是重要的,這個階段是否嚴謹,直接決定了JVM是否能承受惡意代碼的攻擊。
那麼驗證階段具體應當檢查哪些方面?如何檢查?什麼時候檢查呢?
在《Java虛擬機規範(Java SE 7版)》中大概是有130頁左右的篇幅是來描述驗證的過程的,受篇幅所限,咱們沒法逐條規則去探討。但從總體上來看,驗證階段大體會完成以下四個階段的檢驗動做:
1)、文件格式驗證
文件格式驗證就是要驗證字節流是否符合Class文件格式的規範,以及是否能被當前版本的虛擬機處理。例如,常量池的常量中是否有不被支持的常量類型;Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息等等。
這個階段驗證的主要目的就是爲了保證輸入的字節流能正確地解析並存儲於方法區或元數據區以內,格式上符合描述一個Java類型信息的要求。本階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證,字節流纔會正常進入內存(方法區/元數據區)中進行存儲,因此後面剩下的3個驗證階段所有是基於已經載入內存的存儲結構進行的,而不會再直接操做字節流了。
2)、元數據驗證
元數據區驗證的主要目的是對類的元數據信息進行語義的校驗,以保證描述的信息符合Java語言規範的要求。
例如:
這個類是否有父類(除了java.lang.Object以外,全部的類都應該有父類)?這個類的父類是否繼承了不容許被繼承的類(如被final修飾的類)?若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法?類中的字段、方法是否與父類產生矛盾?等等。雖然這些邏輯目前編譯器都會在編譯字節碼文件時加以校驗,可是做爲JVM類加載自己爲了確保自身的安全性,也是須要進行嚴格校驗的。
3)、字節碼驗證
字節碼驗證是一個更加複雜的階段,主要目的是經過數據流和控制流的分析,肯定程序語義是合法的、符合邏輯的。在元數據驗證階段主要是完成了對元數據信息的類型校驗,而這個階段則是對類的方法體進行校驗分析,確保被校驗類的方法在運行時不會作出危害虛擬機安全的事件。例如,保證方法體中的類型轉換是有效的,能夠把一個子類對象賦值給父類的數據類型(上溯造型),可是不能把父類對象賦值給子類數據類型或者把對象賦值給與與它毫無繼承關係的類型。
4)、符號引用驗證
符號引用驗證能夠看作是對類自身之外(主要是常量池中的各類符號引用)的信息進行匹配性校驗。目的是確保後面進入解析階段後,解析動做可以正常執行。若是沒法經過符號引用驗證,就會拋出如「java.lang.IllegalAccessError」、「java.lang.NoSuchFileIdError」、「java.lang.NoSuchMethodError」等這樣的異常信息。
對於JVM的類加載機制來講,驗證階段是一個很是重要,可是不必定必要(對程序的運行期沒有影響)的階段。若是所運行的所有代碼,包括本身編寫的以及第三方包中的代碼都已經被反覆使用和驗證過,那麼就能夠考慮使用
「-Xverify:none」參數來關閉大部分的驗證措施,以縮短虛擬機類加載的時間。
| 準備(Preparation)
準備階段是正式爲類變量(被static修飾的變量)分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區(<Jdk1.8)元數據區(>=Jdk1.8)中進行分配。這時候進行內存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化的時候隨對象一塊兒分配在Java堆中。
另外,上面所說的對類變量進行初始值,一般狀況下是初始爲零值。如int類型的類變量,初始值就是0。
| 解析(Resolution)
在class文件被加載至JVM以前,這個類是沒法知道其餘類及方法、字段所對應的具體地址的,甚至不知道本身方法、字段的內存地址。所以,每當須要引用這些成員時Java編譯器會生成一個符號引用。在運行階段這個符號引用通常都能無歧義地定位在具體目標上。舉個例子,對於一個方法調用,編譯器會生成一個包含目標方法所在類的名字、目標方法的名字、接收參數類型以及返回值類型的符號引用,來指代所要調用的方法。
解析階段的目的就是將這些符號引用解析成爲實際引用。而實際引用就是真正指向內存地址的指針、相對偏移量或能間接定位到目標的句柄。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符這7類符號引用進行。
在前面咱們提到過,解析階段並不必定會在鏈接過程當中完成,由於JVM虛擬機規範並無對此做明確的要求,只是規定了:「若是某些字節碼使用了符號引用,那麼在執行這些字節碼以前,須要完成對這些符號引用的解析」。對於這一點你們不要搞錯了。
| 初始化(Intialization)
類初始化是類加載過程的最後一步,是爲標記爲常量值的字段賦值,以及執行<clinit>方法的過程。那麼什麼樣的字段纔會被標記爲常量值呢?<clinit>方法又是什麼呢?
在Java代碼中若是要初始化一個靜態字段,咱們能夠在聲明時直接賦值,也能夠在靜態代碼塊中對其賦值。在這裏,若是直接賦值的靜態字段被 final 所修飾,而且它的類型是基本類型或字符串時,那麼該字段便會被 Java 編譯器標記成常量值(ConstantValue),其初始化直接由Java 虛擬機完成。
而除此以外的直接賦值操做,以及全部靜態代碼塊中的代碼則都會被Java編譯器置於同一方法中,這個方法就是<clinit>方法,也稱爲類構造器方法。Java 虛擬機會經過加鎖來確保類的 <clinit> 只會被執行一次。
在咱們講述JVM類加載過程的時候,並無特別說明什麼狀況下須要開始類加載過程的第一個階段:加載?這是由於JVM虛擬機規範並無進行強制約束。可是對於初始化階段,JVM規範則是嚴格規定了發生以下狀況時必須馬上對類進行「初始化」,而加載、驗證、準備也天然須要在此以前開始。
這幾種狀況以下:
1)、當虛擬機啓動時,初始化用戶指定的主類(包含main方法的類);
2)、當遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類;
3)、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類;
4)、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類;
5)、子類的初始化會先觸發父類的初始化(若是父類尚未進行過初始化的話);
6)、若是一個接口定義了 default 方法,那麼直接實現或者間接實現該接口的類的初始化,會觸發該接口的初始化;
7)、使用反射 API 對某個類進行反射調用時,初始化這個類;
8)、當初次調用 MethodHandle 實例時(JDK1.7的動態語言支持),初始化該 MethodHandle 指向的方法所在的類;
以上基本上就是類加載機制中初始化的大體過程,只有當初始化完成以後,類才能正式成爲可執行的狀態。
類加載器
在整個類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他的動做徹底是由虛擬機主導和控制的。那麼什麼是類加載器呢?
在上述類加載機制的第一個階段:"加載"中,把「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做由JVM外部實現的代碼模塊稱爲「類加載器」。
從JVM的角度來看,只存在兩種不一樣的類加載器:啓動類加載器(Bootstrap ClassLoader)、其餘類的加載器。啓動類加載器是由C++語言實現的,屬於JVM自身的一部分,而其餘的類加載器則都是獨立於JVM外部,由Java語言實現的繼承java.lang.ClassLoader的類型。
而從Java程序員的角度看,類加載器還能夠劃分得更加細緻一些。示意圖以下:
在上圖中的類加載器,是有層次關係的,這種關係被稱之爲類加載器的「雙親委派模式」,它要求除了頂層啓動類加載器外,其他全部的類加載器都應當有本身的父類加載器,而且若是一個類加載器在收到類加載的請求以後都要先把這個請求委派給父類加載器去完成(每個層次的類加載器都是如此,所以全部的加載請求最終都應該會傳送到頂層的啓動類加載器中),只有當父類加載器反饋本身沒法完成這個加載請求(在搜索範圍沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
雙親委派模式不是強制性的約束模型,只是Java設計者推薦給開發者的類加載器的實現方式,可是採用這種模式對於保證Java程序的穩定運做確實很重要的,由於它能夠避免Java體系中基礎的類型被混亂加載的風險。例如類java.lang.Object,它存放在rt.jar之中,不管那一個類加載器要加載這個類,最終都會委派給啓動類加載器,這樣Object類在程序的各類類加載器環境中都是一個類,不然就會致使系統中出現多個不一樣的Object類,從而連Java類型體系中最基本的行爲都沒法保證。
以上就是關於JVM類加載系統的所有內容了,但願本文可以對你補充知識盲點起到一點做用!