1. 程式人生 > >深入JAVA虛擬機之類加載機制

深入JAVA虛擬機之類加載機制

避免 網絡 平時 java類 循環 靈活性 運用 校驗 特定

前言:

前面學習了類Class文件格式和裏面具體的內容,也已經學習了運行時數據區的各部分區域的內容。接下來就是學習JVM是如何把Class文件中記錄的信息加載到運行時內存中的,以及class文件中各個部分的信息分別存放在運行時數據區的什麽地方。從這篇文字中我們能獲得什麽?

1.虛擬機是如何加載Class文件的
2.Class文件信息進入JVM後有那些變化
3.進一步理解運行時數據區、Class文件信息、以及類加載過程中都做了那些操作、
java語言特性的根基

類加載機制

虛擬機把描述類的數據從Class文件加載到內存中,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的java類型,這就是類加載機制。

看到這裏我有幾個疑問,不知道你們是否有疑問,如下:

1.虛擬機什麽時候加載Class文件到內存的?
2.虛擬機具體是用哪些部件加載Class文件到內存的?
3.在利用某些部件加載Class文件時又具體做了那些操作?

類加載時機
一個類從加載進入內存,到從內存卸載的整個生命周期,一共最多需要經歷:
加載、驗證、準備、解析、初始化、使用、卸載七個階段。
備註:
驗證、準備、解析又統一合稱為鏈接階段。

加載、驗證、準備、初始化、卸載這五個階段順序是確定的,而解析階段則不一定。
它在**某些情況**下可以在初始化之後開始,這就是java為什麽支持**動態綁定**。

那麽我們知道類加載分類這些階段,那麽類加載第一階段:加載階段(Loading)是什麽時候開始的呢?虛擬機規範中沒有強制要求,這一點可以根據虛擬機具體實現來自由把握。但初始化階段在虛擬機規範中有嚴格規定了四種情況必須立即開始初始化階段。

1.遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時。如果沒有初始化,那
麽就必須觸發初始化。
2.使用java.lang.reflect包方法對類進行反射調用時,如果沒有初始化,必須觸發初始化。
3.當初始化一個類時,其父類沒有被初始化,那麽需先觸發父類初始化。
4.當虛擬機啟動時,用戶需要指定一個要執行的主類(main類,主入口),虛擬機會先
初始化這個主類。
備註:有且只有這四種情況,虛擬機會對一個類**主動引用**。除此之外的所有類的引用,
都不會觸發初始化,稱為**被動引用**。

看到這裏,我依然一頭霧水,不知道類加載過程的第一階段:加載階段是神碼時候開始的。那我們再來想想,我們平時是如何運行一個java應用或者程序的?是不是都是從一個主類開始執行run的,因為開發工具幫我們屏蔽了很多底層細節,所以我現在想起來,我們點擊主入口run時,是不是就是啟動了虛擬機,並指定了該主類為要執行的主類,也就是上面所說的第四中情況。但是虛擬機啟動並執行主類的細節我目前也不知道底層做了那些細節操作,具體我先自己在這裏挖個坑,自己後面填上。

到這裏我就知道整個類加載過程應該就是從第四中情況的主類加載開始的。

加載階段
加載階段,虛擬機需要做三件事情:

1.通過一個類的全限定名來獲取定義此類的二進制字節流
2.將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
3.在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入
口

虛擬機規範中的這三點說的並不具體,很寬泛,比如說獲取二進制字節流,並沒有說是從哪裏獲取,可以從class文件中獲取,也可以從網絡中獲取,這就給了開發者極大的靈活性。

從ZIP文件中獲取:最終成為JAR,WAR,EAR格式的基礎。
從網絡中獲取:比如說Applet。
運行時計算生成:運用最多的就是動態代理技術,java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來為特定接口生成.$proxy的代理類的二進制字節流。
由其他文件生成:如jsp
等等其他的獲取方式。同時由誰來加載這個二進制字節流也非常具有靈活性,我們可以使用系統提供的類加載器加載,也可以通過自定義的類加載器進行加載。看到這裏我們也就知道是類加載器把字節流加載到內存中的。

驗證階段
虛擬機規範中沒有明確規定具體要檢查那方面、如何檢查、何時檢查,不同的虛擬機實現可能會有不同,但大都會完成下列四項驗證:
文件格式驗證
需要驗證字節流是否符合Class文件格式的規範。是否能被當前版本虛擬機處理執行。這一項的驗證是根據Class文件格式而來,比如說,魔數是否為0xCAFEBABE?次版本號、主版本號是否當前虛擬機本版可處理範圍?常量池中是否有不被支持的常量類型呀(tag檢查)等等。該驗證是保證輸入的字節流能夠正確地解析並存儲於方法區內。只有通過了文件格式驗證的字節流才能被存儲到方法區,這一驗證是依賴於字節流的。後續的驗證都是基於方法區存儲結構的驗證。
元數據驗證
這一階段驗證是對字節碼描述的信息進行語義分析,驗證是否符合java語言規範。是否有父類,是否繼承不可繼承的類,如果不是抽象類,是否實現了父類的或者接口的方法,類字段、方法、與父類是否有矛盾等等的校驗。

字節碼驗證
這一階段驗證是最復雜的,它主要進行數據流和控制流分析,保證類方法在運行中不會作出危害虛擬機安全的行為。並且註意沒有通過字節碼驗證的肯定是不安全的,但通過了不能就代表一定安全的。比如說死循環。數據流驗證的高復雜性,虛擬機團隊為了避免耗費過多時間在字節碼校驗階段,給方法體的code屬性增加了StackMapTable的屬性,該項優化是在jdk1.6之後的javac編譯器中進行的,通過-XX:-UseSplitVerifier選項進行開啟優化,或者啟動-XX:FailOverToOldVerifier要求失敗後退回到老版本的類型推導驗證。在jdk1.7之後,主版本號大於50的Class文件只能有數據流分析校驗這一種方式。
符號引用驗證
這一階段發生在虛擬機將符號引用轉化為直接引用時觸發,在解析階段中發生。通常校驗:是否能找到類,是否有符合方法的字段描述、簡單名稱所描述的方法、字段,類,方法,字段的訪問修飾是否可以被當前類訪問,等等的校驗。該階段驗證是為了確保解析動作能正常執行。如果該階段未通過,那麽就會拋出java.lang.IncompatibleClassChangeError異常的子類,比如:非法訪問異常,無此字段異常,無此方法異常等。

備註:
驗證階段不是一定要執行的階段,如果在開發測試期間以及反復驗證使用過,那麽可以通過設置:-Xverify:none;關閉大部分的類驗證措施,縮短類加載的時間。

準備階段

這一階段是正式為類變量分配內存設置初始值的階段,都是在方法區分配。此處的初始值,不是程序中給定的字面量,而是系統給定的初始值(也叫做零值)。
技術分享圖片
解析階段
該階段是將常量池中的符號引用轉化為直接引用的階段。主要針對類或者接口、字段、類方法、接口方法四類的符號引用進行解析。

初始化階段
這一階段應該說是開發者很進的,因為這各就是根據java編寫字節碼設置變量的值,或者引用。從另外一個角度看就是執行構造方法的階段,這一階段有些比較細致的行為操作,比如類或者接口他們的父類或者父接口的構造方法,靜態語句快的執行順序等在這裏不做詳細書寫,有興趣可自行查閱資料,或者看深入java虛擬機第七章的初始化階段。

那麽寫到這裏類加載過程就已經寫完了,最後總結一下:
1.類加載過程包括加載、驗證、準備、解析、初始化五個階段。
2.類的加載是由虛擬機的類加載器或開發者自定義的類加載器加載
3.類加載的各個階段都有進行一些操作,這些操作都是圍繞class文件結構、java語言規範、虛擬機安全而進行的。

講到這裏你是否堆類加載過程有了更全面的理解呢?另外關於虛擬機類加載器、自定義類加載器的深入理解 我也會在後面看完書之後進行總結。上面如果我有表述錯誤或者理解錯誤,請留言指正。

深入JAVA虛擬機之類加載機制