文章首發於微信公衆號:BaronTalkjava
上一篇文章咱們介紹了「類文件結構」,這一篇咱們來看看虛擬機是如何加載類的。git
咱們的源代碼通過編譯器編譯成字節碼以後,最終都須要加載到虛擬機以後才能運行。虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制。github
與編譯時須要進行鏈接工做的語言不一樣,Java 語言中類的加載、鏈接和初始化都是在程序運行期間完成的,這種策略雖然會讓類加載時增長一些性能開銷,可是會爲 Java 應用程序提供高度的靈活性,Java 裏天生可動態擴展的語言特性就是依賴運行期間動態加載和動態鏈接的特色實現的。數組
例如,一個面向接口的應用程序,能夠等到運行時再指定實際的實現類;用戶能夠經過 Java 預約義的和自定義的類加載器,讓一個本地的應用程序運行從網絡上或其它地方加載一個二進制流做爲程序代碼的一部分。安全
類從被虛擬機從加載到卸載,整個生命週期包含:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)7 個階段。其中驗證、準備、解析 3 個部分統稱爲鏈接(Linking)。這 7 個階段的發生順序以下圖:微信
上圖中加載、驗證、準備、初始化和卸載 5 個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進的開始「注意,這裏說的是循序漸進的開始,並不要求前一階段執行完才能進入下一階段」,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 的動態綁定。網絡
虛擬機規範中對於何時開始類加載過程的第一節點「加載」並無強制約束。可是對於「初始化」階段,虛擬機則是嚴格規定了有且只有如下 5 種狀況,若是類沒有進行初始化,則必須當即對類進行「初始化」(加載、驗證、準備天然須要在此以前開始):數據結構
「有且只有」以上 5 種場景會觸發類的初始化,這 5 種場景中的行爲稱爲對一個類的主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。好比以下幾種場景就是被動引用:性能
這裏的「加載」是指「類加載」過程的一個階段。在加載階段,虛擬機須要完成如下 3 件事:spa
驗證是鏈接階段的第一步,這一階段的目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體上會完成下面 4 個階段的檢驗動做:
文件格式驗證:第一階段要驗證字節流是否符合 Class 文件格式的規範,而且可以被當前版本的虛擬機處理。驗證點主要包括:是否以魔數 0xCAFEBABE 開頭;主、次版本號是否在當前虛擬機處理範圍以內;常量池的常量中是否有不被支持的常量類型;Class 文件中各個部分及文件自己是否有被刪除的或者附加的其它信息等等。
元數據驗證:第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規範的要求,這個階段的驗證點包括:這個類是否有父類;這個類的父類是否繼承了不容許被繼承的類;若是這個類不是抽象類,是否實現了其父類或者接口之中要求實現的全部方法;類中的字段、方法是否與父類產生矛盾等等。
字節碼驗證:第三階段是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
符號引用驗證:最後一個階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三階段--解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的形象進行匹配性校驗。
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區進行分配。這個階段中有兩個容易產生混淆的概念須要強調下:
首先,這時候進行內存分配的僅包括類變量(被 static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在 Java 堆中;
其次這裏所說的初始值「一般狀況」下是數據類型的零值。假設一個類變量的定義爲public static int value = 123;
那麼變量 value
在準備階段事後的初始值爲 0 而不是 123,由於這個時候還沒有執行任何 Java 方法,而把 value 賦值爲 123 的 putstatic 指令是程序被編譯以後,存放於類構造器 () 方法之中,因此把 value 賦值爲 123 的動做將在初始化階段纔會執行。
這裏提到,在「一般狀況」下初始值是零值,那相對的會有一些「特殊狀況」:若是類字段的字段屬性表中存在 ConstantsValue 屬性,那在準備階段變量 value 就會被初始化爲 ConstantValue 屬性所指的值。假設上面的類變量 value 的定義變爲 public static final int value = 123;
,編譯時 JavaC 將會爲 value 生成 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 123。
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。前面提到過不少次符號引用和直接引用,那麼到底什麼是符號引用和直接引用呢?
符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號能夠上任何形式的字面量,只要使用時能無歧義地定位到目標便可。
直接引用(Direct Reference):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。
類初始化階段是類加載過程當中的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底是由虛擬機主導和控制的。到了初始化階段,才真正開始執行類中定義的 Java 程序代碼。初始階段是執行類構造器 () 方法的過程。
虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到 Java 虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在 Java 虛擬機的惟一性,每一個類加載器都擁有一個獨立的類名稱空間。也就是說:比較兩個類是否「相等」,只要在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。
從 Java 虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用 C++ 來實現,是虛擬機自身的一部分;另外一種就是全部其餘的類加載器,這些類加載器都由 Java 來實現,獨立於虛擬機外部,而且全都繼承自抽象類 java.lang.ClassLoader
。
從 Java 開發者的角度來看,類加載器能夠劃分爲:
啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在 <JAVA_HOME>\lib 目錄中的類庫加載到虛擬機內存中。啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,納智捷使用 null 代替便可;
擴展類加載器(Extension ClassLoader):這個類加載器由 sun.misc.Launcher$ExtClassLoader
實現,它負責加載 <JAVA_HOME>\lib\ext 目錄中,或者被 java.ext.dirs 系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器;
應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$App-ClassLoader
實現。getSystemClassLoader()
方法返回的就是這個類加載器,所以也被稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫。開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
咱們的應用程序都是由這 3 種類加載器互相配合進行加載的,在必要時還能夠本身定義類加載器。它們的關係以下圖所示:
上圖中所呈現出的這種層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器之外,其他的類加載器都應當有本身的父類加載器。
雙親委派模型的工做過程是這樣的:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋本身沒法完成這個類加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
這樣作的好處就是 Java 類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如 java.lang.Object,它放在 rt.jar 中,不管哪個類加載器要加載這個類,最終都是委派給處於模型頂端的啓動類加載器來加載,所以 Object 類在程序的各類類加載器環境中都是同一個類。相反,若是沒有使用雙親委派模型,由各個類加載器自行去加載的話,若是用戶本身編寫了一個稱爲 java.lang.Object 的類,並放在程序的 ClassPath 中,那系統中將會出現多個不一樣的 Object 類,Java 類型體系中最基本的行爲也就沒法保證了。
雙親委派模型對於保證 Java 程序運行的穩定性很重要,但它的實現很簡單,實現雙親委派模型的代碼都集中在 java.lang.ClassLoader 的 loadClass() 方法中,邏輯很清晰:先檢查是否已經被加載過,若沒有則調用父類加載器的 loadClass() 方法,若父加載器爲空則默認使用啓動類加載器做爲父加載器。若是父類加載失敗,拋出 ClassNotFoundException 異常後,再調用本身的 findClass() 方法進行加載。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先,檢查請求的類是否是已經被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 若是父類拋出 ClassNotFoundException 說明父類加載器沒法完成加載
}
if (c == null) {
// 若是父類加載器沒法加載,則調用本身的 findClass 方法來進行類加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
複製代碼
關於類文件結構和類加載就經過連續的兩篇文章介紹到這裏了,下一篇咱們來聊聊「虛擬機的字節碼執行引擎」。
參考資料:
若是你喜歡個人文章,就關注下個人公衆號 BaronTalk 、 知乎專欄 或者在 GitHub 上添個 Star 吧!
- 微信公衆號:BaronTalk
- 知乎專欄:zhuanlan.zhihu.com/baron
- GitHub:github.com/BaronZ88
- 我的博客:baronzhang.com