Java虛擬機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換決議和初始化,最 終形成可以被虛擬機直接使用的Java型別,這個程序被稱作虛擬機的類加載機制,與那些在編譯時需 要進行連接的語言不同,在Java語言里面,型別的加載、連接和初始化程序都是在程式運行期間完成 的,這種策略讓Java語言進行提前編譯會面臨額外的困難,也會讓類加載時稍微增加一些性能開銷, 但是卻為Java應用提供了極高的擴展性和靈活性,Java天生可以動態擴展的語言特性就是依賴運行期動 態加載和動態連接這個特點實作的,例如,撰寫一個面向介面的應用程式,可以等到運行時再指定其 實際的實作類,用戶可以通過Java預置的或自定義類加載器,讓某個本地的應用程式在運行時從網路 或其他地方上加載一個二進制流作為其程式代碼的一部分,這種動態組裝應用的方式目前已廣泛應用 于Java程式之中,從最基礎的Applet、JSP到相對復雜的OSGi技術,都依賴著Java語言運行期類加載才 得以誕生,
v類加載的時機
一個型別從被加載到虛擬機記憶體中開始,到卸載出記憶體為止,它的整個生命周期將會經歷加載 (Loading)、驗證(Verification)、準備(Preparation)、決議(Resolution)、初始化 (Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、決議三個部分統稱 為連接(Linking),如下圖:

如上圖,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,型別的加載程序必須按 照這種順序按部就班地開始,而決議階段則不一定:它在某些情況下可以在初始化階段之后再開始, 這是為了支持Java語言的運行時系結特性(也稱為動態系結或晚期系結),請注意,這里筆者寫的是 按部就班地“開始”,而不是按部就班地“進行”或按部就班地“完成”,強調這點是因為這些階段通常都 是互相交叉地混合進行的,會在一個階段執行的程序中呼叫、激活另一個階段,
關于在什么情況下需要開始類加載程序的第一個階段“加載”,《Java虛擬機規范》中并沒有進行 強制約束,這點可以交給虛擬機的具體實作來自由把握,但是對于初始化階段,《Java虛擬機規范》 則是嚴格規定了有且只有六種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之 前開始):
(1)遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果型別沒有進行過初始 化,則需要先觸發其初始化階段,能夠生成這四條指令的典型Java代碼場景有:
- 使用new關鍵字實體化物件的時候,
- 讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外) 的時候,
- 呼叫一個型別的靜態方法的時候,
(2)使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需 要先觸發其初始化,
(3)當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,
(4)當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先 初始化這個主類,
(5)當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后的解 析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種型別的方法句 柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化,
(6)當一個介面中定義了JDK 8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有 這個介面的實作類發生了初始化,那該介面要在其之前被初始化,
對于這六種會觸發型別進行初始化的場景,《Java虛擬機規范》中使用了一個非常強烈的限定語 ——“有且只有”,這六種場景中的行為稱為對一個型別進行主動參考,除此之外,所有參考型別的方 式都不會觸發初始化,稱為被動參考,下面舉三個例子來說明何為被動參考,分別見代碼清單7-1、代 碼清單7-2和代碼清單7-3,
代碼清單7-1 被動參考的例子之一

上述代碼運行之后,只會輸出“Super-Class init.”,而不會輸出“Node-class init.”,對于靜態欄位, 只有直接定義這個欄位的類才會被初始化,因此通過其子類來參考父類中定義的靜態欄位,只會觸發 父類的初始化而不會觸發子類的初始化,至于是否要觸發子類的加載和驗證階段,在《Java虛擬機規 范》中并未明確規定,所以這點取決于虛擬機的具體實作,對于HotSpot虛擬機來說,可通過-XX: +TraceClassLoading引數觀察到此操作是會導致子類加載的,
代碼清單7-2 被動參考的例子之二

這段代碼復用了代碼清單7-1中的SuperClass,運行之后發現沒有輸出“Super-Class init.”,說明并沒有觸發類com.toutou.sample.book.SuperClass的初始化階段,但是這段代碼里面觸發了 另一個名為“[com.toutou.sample.book.SuperClass”的類的初始化階段,對于用戶代碼來說,這并不是 一個合法的型別名稱,它是一個由虛擬機自動生成的、直接繼承于java.lang.Object的子類,創建動作由 位元組碼指令newarray觸發,
這個類代表了一個元素型別為com.toutou.sample.book.SuperClass的一維陣列,陣列中應有的屬性 和方法(用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實作在這個類里,Java語 言中對陣列的訪問要比C/C++相對安全,很大程度上就是因為這個類包裝了陣列元素的訪問[1],而 C/C++中則是直接翻譯為對陣列指標的移動,在Java語言里,當檢查到發生陣列越界時會拋出 java.lang.ArrayIndexOutOfBoundsException例外,避免了直接造成非法記憶體訪問,
代碼清單7-3 被動參考的例子之三

上述代碼運行之后,也沒有輸出“Const-Class init.”,這是因為雖然在Java原始碼中確實參考了ConstClass類的常量HELLO,但其實在編譯階段通過常量傳播優化,已經將此常量的值“hello world.”直接存盤在NotInitialization類的常量池中,以后NotInitialization對常量 ConstClass.HELLO 的參考,實際都被轉化為NotInitialization類對自身常量池的參考了,也就是 說,實際上NotInitialization的Class檔案之中并沒有ConstClass類的符號參考入口,這兩個類在編譯成 Class檔案后就已不存在任何聯系了,
介面的加載程序與類加載程序稍有不同,針對介面需要做一些特殊說明:介面也有初始化程序, 這點與類是一致的,上面的代碼都是用靜態陳述句塊“static{}”來輸出初始化資訊的,而介面中不能使 用“static{}”陳述句塊,但編譯器仍然會為介面生成“()”類構造器[2],用于初始化介面中所定義的 成員變數,介面與類真正有所區別的是前面講述的六種“有且僅有”需要觸發初始化場景中的第三種: 當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化時,并不要求其父 介面全部都完成了初始化,只有在真正使用到父介面的時候(如參考介面中定義的常量)才會初始 化,
v類加載的程序
接下來我們會詳細了解Java虛擬機中類加載的全程序,即加載、驗證、準備、決議和初始化這五 個階段所執行的具體動作,
? 2.1 加載“加載”(Loading)階段是整個“類加載”(Class Loading)程序中的一個階段,希望讀者沒有混淆 這兩個看起來很相似的名詞,在加載階段,Java虛擬機需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進制位元組流,
2)將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構,
3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口,
《Java虛擬機規范》對這三點要求其實并不是特別具體,留給虛擬機實作與Java應用的靈活度都是 相當大的,例如“通過一個類的全限定名來獲取定義此類的二進制位元組流”這條規則,它并沒有指明二 進制位元組流必須得從某個Class檔案中獲取,確切地說是根本沒有指明要從哪里獲取、如何獲取,僅僅 這一點空隙,Java虛擬機的使用者們就可以在加載階段搭構建出一個相當開放廣闊的舞臺,Java發展歷 程中,充滿創造力的開發人員則在這個舞臺上玩出了各種花樣,許多舉足輕重的Java技術都建立在這 一基礎之上,例如:
- 從ZIP壓縮包中讀取,這很常見,最終成為日后JAR、EAR、WAR格式的基礎,
- 從網路中獲取,這種場景最典型的應用就是Web Applet,
- 運行時計算生成,這種場景使用得最多的就是動態代理技術,在java.lang.reflect.Proxy中,就是用 了ProxyGenerator.generateProxyClass()來為特定介面生成形式為“*$Proxy”的代理類的二進制位元組流,
- 由其他檔案生成,典型場景是JSP應用,由JSP檔案生成對應的Class檔案,
- 從資料庫中讀取,這種場景相對少見些,例如有些中間件服務器(如SAP Netweaver)可以選擇 把程式安裝到資料庫中來完成程式代碼在集群間的分發,
- 可以從加密檔案中獲取,這是典型的防Class檔案被反編譯的保護措施,通過加載時解密Class文 件來保障程式運行邏輯不被窺探,
- ......
相對于類加載程序的其他階段,非陣列型別的加載階段(準確地說,是加載階段中獲取類的二進 制位元組流的動作)是開發人員可控性最強的階段,加載階段既可以使用Java虛擬機里內置的引導類加 載器來完成,也可以由用戶自定義的類加載器去完成,開發人員通過定義自己的類加載器去控制位元組 流的獲取方式(重寫一個類加載器的findClass()或loadClass()方法),實作根據自己的想法來賦予應用 程式獲取運行代碼的動態性,
對于陣列類而言,情況就有所不同,陣列類本身不通過類加載器創建,它是由Java虛擬機直接在 記憶體中動態構造出來的,但陣列類與類加載器仍然有很密切的關系,因為陣列類的元素型別(ElementType,指的是陣列去掉所有維度的型別)最侄訓是要靠類加載器來完成加載,一個陣列類(下面簡稱 為C)創建程序遵循以下規則:
- 如果陣列的組件型別(Component Type,指的是陣列去掉一個維度的型別,注意和前面的元素類 型區分開來)是參考型別,那就遞回采用本節中定義的加載程序去加載這個組件型別,陣列C將被標 識在加載該組件型別的類加載器的類名稱空間上(這點很重要,在7.4節會介紹,一個型別必須與類加 載器一起確定唯一性),
- 如果陣列的組件型別不是參考型別(例如int[]陣列的組件型別為int),Java虛擬機將會把陣列C 標記為與引導類加載器關聯,
- 陣列類的可訪問性與它的組件型別的可訪問性一致,如果組件型別不是參考型別,它的陣列類的 可訪問性將默認為public,可被所有的類和介面訪問到,
加載階段結束后,Java虛擬機外部的二進制位元組流就按照虛擬機所設定的格式存盤在方法區之中 了,方法區中的資料存盤格式完全由虛擬機實作自行定義,《Java虛擬機規范》未規定此區域的具體 資料結構,型別資料妥善安置在方法區之后,會在Java堆記憶體中實體化一個java.lang.Class類的物件, 這個物件將作為程式訪問方法區中的型別資料的外部介面,
加載階段與連接階段的部分動作(如一部分位元組碼檔案格式驗證動作)是交叉進行的,加載階段 尚未完成,連接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的一部 分,這兩個階段的開始時間仍然保持著固定的先后順序,
v博客總結
特別宣告:本文非原創/非商用,只為記錄讀書筆記,全篇絕大部分內容(偶爾會加入一些博主讀后感)摘抄自《深入理解Java虛擬機(第3版)》,閱讀原文,請購買正版書籍,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/163252.html
標籤:Java
