之前關於反射的文章也說過java類的加載過程,但是因爲主題原因沒有很系統的介紹,這裏系統學習介紹下。
一、類的生命週期
一個類型從被加載到虛擬機內存中開始,到卸載出內存爲止,整個生命週期將會經歷加載(Loading )、驗證( Verification )、準備( Preparation )、解析( Resolution )、初始化(Initialization )、使用( Using )和卸載( Unloading )七個階段,其中驗證、準備、解析三個部分統稱爲連接(Linking )。
類的生命週期
二、類加載的過程
類加載的全過程,即加載、驗證、準備、解析和初始化這五個階段所執行的具體動作。
2.1 加載
在加載階段, Java 虛擬機需要完成以下三件事情:
1 )通過一個類的全限定名來獲取定義此類的二進制字節流。
2 )將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
3 )在內存中生成一個代表這個類的 java.lang.Class 對象,作爲方法區這個類的各種數據的訪問入 口。
這裏的二進制字節流並非必須得從某個Class 文件中獲取,具體可以從:
從 ZIP 壓縮包中讀取,這很常見,最終成爲日後 JAR 、 EAR 、 WAR 格式的基礎。
從網絡中獲取,這種場景最典型的應用就是 Web Applet 。
運行時計算生成,這種場景使用得最多的就是動態代理技術,在 java.lang.reflect.Proxy 中,就是用 了 ProxyGenerator.generateProxyClass() 來爲特定接口生成形式爲 “*$Proxy” 的代理類的二進制字節流。
由其他文件生成,典型場景是 JSP 應用,由 JSP 文件生成對應的 Class 文件。
從數據庫中讀取,這種場景相對少見些,例如有些中間件服務器(如 SAP Netweaver )可以選擇 把程序安裝到數據庫中來完成程序代碼在集羣間的分發。
可以從加密文件中獲取,這是典型的防 Class 文件被反編譯的保護措施,通過加載時解密 Class 文 件來保障程序運行邏輯不被窺探。
加載階段結束後, Java 虛擬機外部的二進制字節流就按照虛擬機所設定的格式存儲在方法區之中了,方法區中的數據存儲格式完全由虛擬機實現自行定義,《Java 虛擬機規範》未規定此區域的具體數據結構。類型數據妥善安置在方法區之後,會在Java 堆內存中實例化一個 java.lang.Class 類的對象,這個對象將作爲程序訪問方法區中的類型數據的外部接口。
加載階段與連接階段的部分動作(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬於連接階段的一部分,這兩個階段的開始時間仍然保持着固定的先後順序。
2.2 驗證
驗證的目的是確保 Class 文件的字節流中包含的信息符合《 Java 虛擬機規範》的全部約束要求,保證這些信息被當作代碼運行後不會危害虛擬機自身的安全。
Java 語言本身是相對安全的編程語言(起碼對於 C/C++ 來說是相對安全的),使用純粹的 Java 代碼無法做到諸如訪問數組邊界以外的數據、將一個對象轉型爲它並未實現的類型、跳轉到不存在的代碼行之類的事情,如果嘗試這樣去做了,編譯器會毫不留情地拋出異常、拒絕編譯。但前面也曾說過, Class文件並不一定只能由 Java 源碼編譯而來,它可以使用包括靠鍵盤 0 和 1 直接在二進制編輯器中敲出 Class文件在內的任何途徑產生。上述 Java 代碼無法做到的事情在字節碼層面上都是可以實現的,至少 語義上是可以表達出來的。Java 虛擬機如果不檢查輸入的字節流,對其完全信任的話,很可能會因爲載入了有錯誤或有惡意企圖的字節碼流而導致整個系統受攻擊甚至崩潰,所以驗證字節碼是Java 虛擬機保護自身的一項必要措施。
驗證階段大致上會完成下面四個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
2.2.1 文件格式驗證
要驗證字節流是否符合 Class文件格式的規範,比如:
主、次版本號是否在當前 Java虛擬機接受範圍之內;
常量池的常量中是否有不被支持的常量類型;
指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量;
只有通過了這個階段的驗證之後,這段字節流才被允許進入Java 虛擬機內存的方法區中進行存儲,所以後面的三個驗證階段全部是基於方法區的存儲結構上進行的,不會再直接讀取、操作字節流了。
2.2.2 元數據驗證
元數據驗證是對字節碼描述的信息進行語義分析,以保證其描述的信息符合《Java 語言規範》的要求,這個階段可能包括的驗證點如下:
這個類是否有父類(除了 java.lang.Object 之外,所有的類都應當有父類)。
這個類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)。
如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法。
2.2.3 字節碼驗證
字節碼驗證主要目的是通過數據流分析和控制流分析,確定程序語義是合法的、符合邏輯的。例如:
2.2.4 符號引用驗證
符號引用驗證發生在虛擬機將符號引用轉化爲直接引用 的時候,這個轉化動作將在連接的第三階段—— 解析階段中發生。符號引用驗證可以看作是對類自身以外的各類信息進行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、字段等資源。例如:
符號引用中通過字符串描述的全限定名是否能找到對應的類。
在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
符號引用中的類、字段、方法的可訪問性( private 、 protected 、 public 、 <package> )是否可被當 前類訪問。
驗證階段對於虛擬機的類加載機制來說,是一個非常重要的、但卻不是必須要執行的階段,因爲驗證階段只有通過或者不通過的差別,只要通過了驗證,其後就對程序運行期沒有任何影響了。如果程序運行的全部代碼(包括自己編寫的、第三方包中的、從外部加載的、動態生成的等所有代碼)都已經被反覆使用和驗證過,在生產環境的實施階段就可以考慮使用-Xverify : none 參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
2.3 準備
準備階段是正式爲類中定義的靜態變量(被 static 修飾的變量)分配內存並設置類變量初始值的階段。
2.4 解析
解析階段是 Java 虛擬機將常量池內的符號引用替換爲直接引用的過程。
符號引用在 Class 文件中它以 CONSTANT_Class_info 、 CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等類型的常量出現。 符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
直接引用是可以直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局直接相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在虛擬機的內存中存在。
2.4 初始化
進行準備階段時,變量已經賦過一次系統要求的初始零值,而在初始化階段,則會根據程序員通過程序編碼制定的主觀計劃去初始化類變量和其他資源。
三、類和類加載器
類與類加載器的關係是:對於任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。 這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
這個結論很重要!這是設計“雙親委派”模型的原因。
四、雙親委派 模型
站在 Java 虛擬機的角度來看,只存在兩種不同的類加載器:一種是啓動類加載器( Bootstrap ClassLoader),這個類加載器使用 C++ 語言實現 ,是虛擬機自身的一部分;另外一種就是其他所有的類加載器,這些類加載器都由Java 語言實現,獨立存在於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。
啓動類加載器( Bootstrap Class Loader ):這個類加載器負責加載存放在 <JAVA_HOME>\lib 目錄,或者被 -Xbootclasspath 參數所指定的路徑中存放的,而且是 Java 虛擬機能夠 識別的(按照文件名識別,如 rt.jar 、 tools.jar ,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類 庫加載到虛擬機的內存中。啓動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時, 如果需要把加載請求委派給引導類加載器去處理,那直接使用 null 代替即可。
擴展類加載器(Extension Class Loader):這個類加載器是在類sun.misc.Launcher$ExtClassLoader中以 Java 代碼的形式實現的。它負責加載 <JAVA_HOME>\lib\ext 目錄中,或者被 java.ext.dirs 系統變量所 指定的路徑中所有的類庫。根據 “ 擴展類加載器 ” 這個名稱,就可以推斷出這是一種 Java 系統類庫的擴 展機制, JDK 的開發團隊允許用戶將具有通用性的類庫放置在 ext 目錄裏以擴展 Java SE 的功能,在 JDK 9 之後,這種擴展機制被模塊化帶來的天然的擴展能力所取代。由於擴展類加載器是由 Java 代碼實現 的,開發者可以直接在程序中使用擴展類加載器來加載 Class 文件。
應用程序類加載器(Application Class Loader):這個類加載器由sun.misc.Launcher$AppClassLoader 來實現。由於應用程序類加載器是 ClassLoader 類中的 getSystem- ClassLoader() 方法的返回值,所以有些場合中也稱它爲 “ 系統類加載器 ” 。它負責加載用戶類路徑 ( ClassPath )上所有的類庫,開發者同樣可以直接在代碼中使用這個類加載器。如果應用程序中沒有 自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
“ 雙親委派模型(Parents Delegation Model)”就是各種類加載器之間的層次關係。 雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應有自己的父類加載器。
雙親委派模型的工作過程是: 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去完成加載。
雙親委派的好處就是所有類的加載最終都會匯聚到啓動類加載器來加載,根據前面類和類加載器的關係介紹,
類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,這樣就使得這個類在程序的各種類加載器環境中都能夠保證是同一個類。 如果沒有使用雙親委派模型,都由各個類加載器自行去加載的話,如果用戶自己也編寫了一個名爲java.lang.Object的類,並放在程序的
ClassPath 中,那系統中就會出現多個不同的 Object 類, Java 類型體系中最基礎的行爲也就無從保證,應用程序將會一片混亂。