現現在,各類IDE愈來愈智能,咱們程序員的平常開發基本上都是在IDE上完成的,它能夠幫助咱們將更多的注意力放在實際的業務處理中,隨着這種安逸的編碼生活的持續,咱們慢慢也就忘記了代碼運行的底層原理。若是不學習,好像也沒啥問題,畢竟咱們的關注重點是代碼邏輯實現上,當出現問題了,百度,谷歌一下,或者問問公司的狠人,問題好像也能愉快的解決,本身好像也理解了似的。但事實上呢,依此周而復始,仍舊不理解,學習一門技術,只有咱們真正懂得了其底層原理,才能更好的解決問題。html
咱們在前面幾篇文章中分別講解了類文件結構,JVM內存管理。這兩篇文章詳細描述了Class文件存儲格式的具體細節及JVM運行時數據區。而今天這篇文章將會講解Class文件中的信息進入到虛擬機中會發生什麼變化。java
**先來個官方敘述:**類加載是Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化、最終造成能夠被虛擬機直接使用的Java類型。通俗來說,就是咱們在完成代碼的編寫後,編譯器會將咱們的java文件編譯成對應的class文件(二進制字節碼文件),經過類載器將這些class的時候將其加載到JVM中,生成對應的class對象。下面,讓咱們詳細來分析下類加載過程。程序員
對於任意一個類,類加載過程能夠分爲加載
、驗證
、準備
、解析
、使用
和卸載
七個階段,以下圖所示:安全
圖中的加載
、驗證
、準備
、初始化
和卸載
這五個階段的順序是肯定的,而解析
則不必定,爲了支持java語言的運行時綁定特性
,解析這個階段能夠發生在初始化階段後。接下來咱們詳細分析類加載過程當中這幾個模塊的做用。markdown
類加載
階段是將字節碼文件.Class的二進制數據讀入內存中的方法區中,而後在堆中建立一個Java.lang.Class對象
,對於加載階段的任意一個類都對應着一個Class類型的對象,能夠經過getClass()
來獲取。對於肯定的類Class,不管該類生成多少個對象,其Class類型的對象只有一個,Class類是整個反射的入口。數據結構
所以,在類加載階段,Java虛擬機主要完成如下幾類任務:oop
驗證是鏈接階段的第一步,其目的是爲了確保Class文件內的字節流包含的信息符是否符合Java虛擬機規範的要求,保證輸入的字節流不會危害到虛擬機自身的安全。咱們也許會有疑問,咱們印象中的Java語言是一門相對安全的語言啊(相比較於C++),如單純的使用Java代碼是沒法訪問到邊界之外的數據,若是咱們非要這麼作,編譯器就會拒絕編譯。可是,回到字節碼層面,一切都變得不可控起來,這是由於Class文件能夠採用不少途徑來產生,並不必定要求用Java源碼編譯出來,若是JVM虛擬機不檢查輸入的字節流,對其徹底信任的話,極可能就會由於載入有害的的字節流致使系統的崩潰。所以,驗證階段在類加載過程當中佔有很大的比重,它驗證的項目能夠大體分爲如下幾個:文件格式的驗證、元數據驗證、字節碼驗證和符號引用驗證,下面咱們一一介紹:學習
文件格式的驗證就是檢查字節流是否符合Class文件格式的規範,不熟悉Class文件格式的能夠看個人上一篇文章類文件結構,文件格式一般檢查一下幾個要素:編碼
魔數,是否以0xCAFEBABE開頭spa
主次版本號是否在合適的範圍
常量池中的常量是否有不被支持的常量類型
指向常量的各類索引值是否有指向不存在的常量或者不符合類型的常量
...........
元數據的驗證是對字節碼描述的信息進行語義分析,驗證的要素主要包含如下幾點:
- 是否有父類,除了Object外,都有父類
- 這個類的父類是否繼承被final修飾的類
- 若這個類不是抽象類,是否實現了父類中的全部方法
- ...........
字節碼驗證是整個驗證過程當中的最複雜的一個階段,它主要經過數據流
和控制流
分析,肯定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型作完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件,例如:
- 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做棧放置了一個int類型的數據,使用時卻按long類型來加載入本地變量表中。
- 保證跳轉指令不會跳轉到方法體之外的字節碼指令上。
- 保證方法體中的類型轉換是有效地,例如能夠把一個子類對象賦值給父類數據類型,這是安全的,可是把父類對象複製給子類數據類型,甚至把對象賦值給與他毫無繼承關係、徹底不相干的一個數據類型,則是危險和不合法的。
- .........
符號引用驗證能夠看作是對類自身(常量池中的各類符號引用)的信息進行匹配性校驗
,它的目的是確保解析動做可以正常執行,若是沒法經過符號的引用驗證,則會拋出異常。符號引用驗證階段一般須要校驗如下內容:
- 符號引用中經過字符串描述的全限定名是否能找到對應的類。
- 在制定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
- 符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問。
......
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量)
,而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。這裏所說的初始值「一般狀況」下是數據類型的零值。
public static int number=10
複製代碼
類變量number在準備階段值是0而不是10,由於這時候還沒有開始執行任何Java方法,而把number賦值爲10的putstatic指令是程序被編譯後,存放於類構造器()方法之中,因此把number賦值爲10的動做將在初始化階段纔會執行。下表列出了全部Java基礎類型的零值:
解析階段就是將Class中的常量池中的符號引用
解析爲直接引用
。符號引是使用一組符號描述被引用的目標,引用的目標不必定加載到內存中;直接引用可使直接指向目標地址的指針,相對偏移量或者間接定位到目標的句柄,有了直接引用,引用的目標必定存在在虛擬機中。主要包括四種類型引用的解析,分別是類或接口解析、字段解析、方法解析和接口方法解析。下面以字段解析和方法解析爲例:
初始化是類加載過程的最後一步,到了初始化階段,纔開始正真的執行字節碼文件,根據字節碼文件的內容對類的各個字段進行賦值;初始化是執行類構造器()方法的過程。實際上,在鏈接的準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員本身寫的邏輯去初始化類變量和其餘資源,舉例以下:
public static int number1 = 5;
public static int number2 = 6;
static{
number = 68;
}
複製代碼
在準備階段number1和number2都等於0;在初始化階段number1和number2分別等於5和6。
總結一下初始化發生的條件:
- 建立一個新的對象實例時(好比new、反射、序列化)
- 調用一個類型的靜態方法時(即在字節碼中執行invokestatic指令)
- 調用一個類型或接口的靜態字段,或者對這些靜態字段執行賦值操做時(即在字節碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態字段除外,它被初始化爲一個編譯時常量表達式
- 調用JavaAPI中的反射方法時(好比調用java.lang.Class中的方法,或者java.lang.reflect包中其餘類的方法)
- 初始化一個類的派生類時(Java虛擬機規範明確要求初始化一個類時,它的超類必須提早完成初始化操做,接口例外)
- JVM啓動包含main方法的啓動類時。
使用階段是當執行完初始化後,就能夠根據本身的實際須要使用具體的類;當咱們在程序中執行System.exit(),加載的類會從內存中卸載,一般狀況下,當程序正常執行結束後、或者發生錯誤而終止都會使得已加載的類對象被卸載。
經過以上的講解,咱們知道了類Class文件被虛擬機加載、使用直至卸載須要經歷的步驟,可是咱們忽略了一個很是重要的問題,類是如何被加載器加載的,加載器須要知足什麼樣的規律?下面咱們一一來說解。
類的加載是使用類加載器經過查詢路徑的方式進行的,加載階段既可使用虛擬機裏內置的引導類加載器來完成,也能夠由用戶自定義類加載器來完成Java中的類加載器一般分爲四類:啓動類加載器
(Bootstrap ClassLoader)、擴展類加載器
(Extension ClassLoader)、應用程序類加載器
(Application ClassLoader)、用戶自定義類加載器
(User ClassLoader)。不一樣的類加載器負責不一樣區域的類的加載。
啓動類加載器
啓動類加載器是加載存放在<JAVA_HOME>\lib目錄,或者被Xbootclasspath選項指定的jar包,如rt.jar、tools.jar。Java中的不少組件都是經過啓動類加載器來完成的,不只如此,擴展類加載器和應用程序類加載器也是由它來加載的。
擴展類加載器
擴展類加載器是加載<JAVA_HOME>\lib\ext*.jar或者-java.ext.dirs指定目錄下的jar包,它的做用是與啓動類加載器配合用於完成系統組件的加載。
應用程序類加載器
應用程序類加載器是加載Classpath或java.class.path所指定的目錄下的類和jar包,一般狀況下,咱們自定義的類都是經過這類加載器完成的。
用戶自定義類加載器
用戶自定義類加載器是經過java.lang.ClassLoader的子類自定義加載class。
上面咱們講到不一樣的類加載器都有不一樣的加載範圍,當某個類加載器要加載某個.class文件時,它首先把這個任務委託給他的上級類加載器,遞歸這個操做,若是上級的類加載器沒有加載,本身才會去加載這個類。所以,不一樣類加載器相互配合就造成類雙親委派模型。
咱們先分析如下加載流程:
咱們在上圖能夠看到,除了啓動類加載器,每個類加載器都有一個父類加載器。當一個類加載器加載一個類時,首先會把加載動做委派給他的父加載器,若是父加載器沒法完成這個加載動做時才由該類加載器進行加載。因爲類加載器會向上傳遞加載請求,因此一個類加載時,首先嚐試加載它的確定是啓動類加載器(逐級向上傳遞請求,直到啓動類加載器,它沒有父加載器),以後根據是否能加載的結果逐級讓子類加載器嘗試加載,直到加載成功。
雙親委派模型的做用:
- 防止重複加載同一個
.class
。經過委託去向上面問一問,加載過了,就不用再加載一遍。保證數據安全- 保證核心
.class
不能被篡改。經過委託方式,不會去篡改核心.clas
,即便篡改也不會去加載,即便加載也不會是同一個.class
對象了。不一樣的加載器加載同一個.class
也不是同一個Class
對象。這樣保證了Class
執行安全。
參考文獻:
[1]周志華.深刻理解Java虛擬機(第三版)
[2]https://blog.csdn.net/en_joker/article/details/79959330
[3]https://www.cnblogs.com/aspirant/p/7200523.html