本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:java
個人GIthub博客git
今天想跟你們嘮嗑嘮嗑Java
的類加載機制,這是Java
的一個很重要的創新點,曾經也是Java
流行的重要緣由之一。github
Oracle
當初引入這個機制是爲了知足Java Applet
開發的需求,JVM
咬咬牙引入了Java
類加載機制,後來的基於Jvm
的動態部署,插件化開發包括你們熱議的熱修復,總之不少後來的技術都源於在JVM
中引入了類加載器。面試
現在,類加載機制也在各個領域大放異彩,在面試中,由類加載機制所衍生出來各種面試題也層出不窮。數據庫
因此,咱們要了解下類加載機制,爲工做中或者是面試中實際的須要打好良好的基礎。數組
Q1:JVM
類加載機制定義:安全
虛擬機把描述類的數據從Class
文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成可被虛擬機直接使用的Java
類型的過程網絡
Q2:特性數據結構
運行期類加載。即在Java
語言裏面,類型的加載、鏈接和初始化過程都是在程序運行期完成的,從而經過犧牲一些性能開銷來換取Java
程序的高度靈活性多線程
什麼是運行期,什麼是編譯期?
- 編譯期是指編譯器將源代碼翻譯爲機器能識別的代碼,
Java
被編譯爲Jvm
認識的字節碼文件- 運行期則是指
Java
代碼的運行過程
JVM
運行期動態加載+動態鏈接->Java
的動態擴展特性
類從被加載到虛擬機內存中開始、到卸載出內存爲止,整個生命週期包括七個階段:
加載
驗證
準備
解析
初始化
使用
卸載
其中,驗證、準備、解析這3個部分統稱爲鏈接,流程以下圖:
注意:
- 『加載』->『驗證』->『準備』->『初始化』->『卸載』這五個階段的順序是肯定的,而『解析』可能爲了支持
Java
的動態綁定會在『初始化』後纔開始- 上述階段一般都是互相交叉地混合式進行的,好比會在一個階段執行的過程當中調用、激活另一個階段
想要了解Java
動態綁定和靜態綁定區別的話,能夠看下這篇文章:理解靜態綁定與動態綁定
Q1:任務
ZIP
包讀取、從網絡中獲取、經過運行時計算生成、由其餘文件生成、從數據庫中讀取等等途徑......想要詳細瞭解類的全限定名的知識,能夠看下這篇文章:全限定名、簡單名稱和描述符是什麼東西?
java.lang.Class
對象,它將做爲程序訪問方法區中的這些類型數據的外部接口JVM
類加載子系統中佔了至關大的一部分Class
文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全因而可知,它能直接決定
JVM
可否承受惡意代碼的攻擊,所以驗證階段很重要,但因爲它對程序運行期沒有影響,並不必定必要,能夠考慮使用-Xverify:none
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
檢驗過程包括下面四個階段:
A.文件格式驗證:
內容:驗證字節流是否符合
Class
文件格式的規範、以及是否能被當前版本的虛擬機處理目的:保證輸入的字節流能正確地解析並存儲於方法區以內,且格式上符合描述一個
Java
類型信息的要求。只有保證二進制字節流經過了該驗證後,它纔會進入內存的方法區中進行存儲,因此後續3個驗證階段所有是基於方法區而不是字節流了例子:
是否以魔數
0xCAFEBABE
開頭主次版本號是否在
JVM
接受範圍內索引值是否有指向不存在/不符合類型的常量
......
B.元數據驗證:
內容:對字節碼描述的信息進行語義分析,以保證其描述的信息符合
Java
語言規範的要求目的:對類的元數據信息進行語義校驗,保證不存在不符合
Java
語言規範的元數據信息例子:
類是否有父類(除了
java.lang.Object
以外,全部類都應有父類)父類是否繼承了不容許被繼承的類(
final
修飾的類)若是該類不是抽象類,是否實現了其父類或接口中要求實現的全部方法
......
C.字節碼驗證:
是驗證過程當中最複雜的一個階段
內容:對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件
目的:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的
例子:
保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現「在操做數棧的數據類型中放置了
int
類型的數據,使用時卻按long
類型來載入本地變量表中」保證任何跳轉指令都不會跳轉到方法體外的字節碼指令上
......
D.符號引用驗證:
- 內容:對類自身之外(如常量池中的各類符號引用)的信息進行匹配性校驗
- 目的:確保解析動做能正常執行,若是沒法經過符號引用驗證,那麼將會拋出一個
java.lang.IncompatibleClassChangeError
異常的子類- 注意:該驗證發生在虛擬機將符號引用轉化爲直接引用的時候,即『解析』階段
Q1:任務
Java
堆中以前提過,解析階段就是虛擬機將常量池內的符號引用替換爲直接引用的過程
- 能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可
- 與虛擬機實現的內存佈局無關,由於符號引用的字面量形式明肯定義在
Java
虛擬機規範的Class
文件格式中,因此即便各類虛擬機實現的內存佈局不一樣,可是能接受符號引用都是一致的
- 能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄
- 與虛擬機實現的內存佈局相關,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不一樣
JVM
會根據須要來判斷,是在類被加載器加載時就對常量池中的符號引用進行解析,仍是等到一個符號引用將要被使用前纔去解析
- 類或接口(
CONSTANT_Class_info
)- 字段(
CONSTANT_Fieldref_info
)- 類方法(
CONSTANT_Methodref_info
)- 接口方法(
CONSTANT_InterfaceMethodref_info
)- 方法類型(
CONSTANT_MethodType_info
)- 方法句柄(
CONSTANT_MethodHandle_info
)- 調用點限定符(
CONSTANT_InvokeDynamic_info
)
舉個例子,設當前代碼所處的爲類D
,把一個從未解析過的符號引用N
解析爲一個類或接口C
的直接引用,解析過程分三步:
- 若
C
不是數組類型:JVM
將會把表明N
的全限定名傳遞給D
類加載器去加載這個類C
。在加載過程當中,因爲元數據驗證、字節碼驗證的須要,又可能觸發其餘相關類的加載動做。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。- 若
C
是數組類型且數組元素類型爲對象:JVM
也會按照上述規則加載數組元素類型- 若上述步驟無任何異常:此時
C
在JVM
中已成爲一個有效的類或接口,但在解析完成前還需進行符號引用驗證,來確認D
是否具有對C
的訪問權限。若是發現不具有訪問權限,將拋出java.lang.IllegalAccessError
異常
Q1:字段(成員變量/域)和屬性有什麼區別?
- 屬性,是指對象的屬性,對於
JavaBean
來講,是getXXX
方法定義的- 字段,是成員變量
class Person{
private String mingzi; //mingzi是字段,通常來講字段和屬性是相同的,可是這個例子是特例
public String getName(){ //name是屬性
return mingzi:
}
public void setName(){
mingzi= "張三";
}
}
複製代碼
Java
代碼。而以前的類加載過程當中,除了在『加載』階段用戶應用程序可經過自定義類加載器參與以外,其他階段均由虛擬機主導和控制
- 準備階段:變量賦初始零值
- 初始化階段:根據Java程序的設定去初始化類變量和其餘資源,或者說是執行類構造器
clinit
的過程
clinit
:由編譯器自動收集類中的全部類變量(靜態變量)的賦值動做和靜態語句塊static{}
中的語句合併產生
- 是線程安全的,在多線程環境中被正確地加鎖、同步
- 對於類或接口來講是非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成
clinit
- 接口與類不一樣的是,執行接口的
clinit
不須要先執行父接口的clinit
,只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的clinit
想詳細瞭解clinit
以及其與init
的區別的讀者,能夠看下這篇文章:深刻理解jvm--Java中init和clinit區別徹底解析
- 遇到
new
、getstatic
、putstatic
或invokestatic
這4條字節碼指令時- 使用
java.lang.reflect
包的方法對類進行反射調用的時候- 當初始化一個類的時候,若發現其父類還未進行初始化,需先觸發其父類的初始化
- 在虛擬機啓動時,需指定一個要執行的主類,虛擬機會先初始化它
- 當使用
JDK1.7
的動態語言支持時,若一個java.lang.invoke.MethodHandle
實例最後的解析結果爲REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,且這個方法句柄所對應的類未進行初始化,需先觸發其初始化。
每一個類加載器,都擁有一個獨立的命名空間,它不只用於加載類,還和這個類自己一塊兒做爲在
JVM
中的惟一標識。因此比較兩個類是否相等,只要看它們是否由同一個類加載器加載,即便它們來源於同一個Class
文件且被同一個JVM
加載,只要加載它們的類加載器不一樣,這兩個類就一定不相等
從JVM
的角度,可將類加載器分爲兩種:
- 由
C++
語言實現,是虛擬機自身的一部分- 負責加載存放在
<JAVA_HOME>\lib
目錄中、或被-Xbootclasspath
參數所指定路徑中的、且可被虛擬機識別的類庫- 沒法被
Java
程序直接引用,若是自定義類加載器想要把加載請求委派給引導類加載器的話,可直接用null
代替
Java
語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader
,可被Java
程序直接引用。常見幾種:
擴展類加載器
A.由
sun.misc.Launcher$ExtClassLoader
實現B.負責加載
<JAVA_HOME>\lib\ext
目錄中的、或者被java.ext.dirs
系統變量所指定的路徑中的全部類庫應用程序類加載器
A.是默認的類加載器,是
ClassLoader#getSystemClassLoader()
的返回值,故又稱爲系統類加載器B.由
sun.misc.Launcher$App-ClassLoader
實現C.負責加載用戶類路徑上所指定的類庫
自定義類加載器:若是以上類加載起不能知足需求,可自定義
須要注意的是:雖然數組類不經過類加載器建立而是由
JVM
直接建立的,但仍與類加載器有密切關係,由於數組類的元素類型最終還要靠類加載器去建立
Java
設計者推薦給開發者的一種類加載器實現方式Java
程序的穩定運做;實現簡單,全部實現代碼都集中在java.lang.ClassLoader的loadClass()
中好比,某些類加載器要加載
java.lang.Object
類,最終都會委派給最頂端的啓動類加載器去加載,這樣Object
類在程序的各類類加載器環境中都是同一個類。相反,系統中將會出現多個不一樣的
Object
類,Java
類型體系中最基礎的行爲也就沒法保證,應用程序也將會變得一片混亂
恭喜你!已經看完了前面的文章,相信你對
JVM
類加載機制已經有必定深度的瞭解,下面,進行一下課堂小測試,驗證一下本身的學習成果吧!
Q1:類加載的全過程是怎樣的?
Q2:什麼是雙親委派模型?
Q3:String
類如何被加載的
上面問題的答案,在前文都提到過,若是還不能回答出來的話,建議回顧下前文
Q4:請你談談類加載過程,以Person a = new Person();
爲例進行說明
這道題是在牛客的暑假實習
Tencent
一面的麪筋上找的,附上標準答案:類的加載過程,Person person = new Person();爲例進行說明
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: