深入理解Java虛擬機(第七章):虛擬機類加載機制

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

類的生命週期:
從被加載到JVM開始,直到卸載出內存:
加載(loading)—驗證(verification)—-準備(preparation)—-解析(resolution)
—-初始化(initialization)—使用(using)—-卸載(unloading)

驗證、準備和解析統稱爲連接;

其中解析階段的發生時機不一定,可能在初始化之後(動態綁定);

五種情況必須立即對類進行初始化(有且僅有這五種情況):
(1)遇到new、getstatic、putstatic、invokestatic這四條指令時,如果類沒有初始化,需先觸發其初始化。(使用new關鍵字實例化對象時、讀取或者設置一個類的靜態字段(除了被final修飾,編譯器就把結果放進常量池的)、以及調用一個類的靜態方法的時候)
(2)使用java.lang.reflect包內方法對類進行反射調用的時候,如果沒初始化,就先初始化;
(3)初始化一個類,如果其父類沒有初始化,則先初始化其父類;
(4)啓動JVM時,初始化包含main方法的主類
(5)當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,並這個句柄對應的類,沒有進行初始化,則先初始化;
以上五種稱爲主動引用,除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用;

類加載的過程
一、加載
在加載階段,java虛擬機需要完成以下3件事:
a.通過一個類的全限定名來獲取定義此類的二進制字節流。
b.將定義類的二進制字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構
c.在java堆中生成一個代表該類的java.lang.Class對象,作爲方法區數據的訪問入口。

加載階段完成後,JVM外部的二進制字節流就按照JVM所需的格式存儲在方法區之中(數據的存儲格式由JVM定義),然後在內存中實例化一個java.lang.Class類的對象;

二、驗證(class文件不一定是java文件編譯來的)
目的是爲了確保,class文件的字節流中包含的信息符合當前JVM的需求,並且不會危害JVM自身安全;
(工作量比例很大)
1、文件格式驗證(與加載過程交叉進行)
驗證字節流是否符合class文件格式規範,並且能否被當前版本JVM處理;
只有經過這個階段的驗證,字節流纔會進入內存的方法區中存儲,所以後邊三個階段的驗證都是基於方法區的存儲結構進行的,不會再操作字節流;
2、元數據驗證
對字節碼描述的信息進行語義分析,以保證描述信息符合java語言規範的要求;
3、字節碼驗證(驗證的過程中,最複雜的階段)
通過數據流和控制流分析,確定程序語義事都是合法的、符合邏輯的;
由於太複雜,所以優化,只需要檢查StackMapTable屬性中的記錄是否合法即可;(可能被篡改,所以不一定安全)
4、符號引用驗證
確保解析動作能正確執行:符號引用是否能找到確切的直接引用;解析就是將符號引用轉爲直接引用;

以上四個階段都是驗證的一部分;非常重要但是不一定一定要進行的階段。因爲有些代碼都是經過反覆使用和驗證過的,很安全,因此可以考慮使用-Xverify:none參數來關閉大部分的類驗證措施;

三、準備
爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配;

類變量:被static修飾的變量,不包括實例變量,實例變量將在對象實例化的時候隨對象分配到java堆中;
變量初始值:賦值爲0;真正的賦值動作在初始化階段執行;
(在編譯時javac會爲static變量設置ConstantValue屬性,在準備階段就按照該屬性將真正的值賦值;比如:final關鍵字修飾)

四:解析

JVM將常量池中的符號引用轉爲直接引用(直接引用則引用目標一定在內存中了);
1、類和接口的解析
2、字段解析
3、類方法解析
4、接口方法解析

五、初始化
初始化是類加載過程最後一步;除了在加載階段用戶可以自定義類加載器參與外,其餘步驟全部由JVM主導和控制;;到了初始化階段纔可以真正執行Java程序代碼;
初始化階段,將變量的真正初始值賦值給變量和其他資源;
初始化過程就是執行類構造器()方法的過程

1、<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問如下代碼;
public class Test{
static{
i=0;//給變量賦值可以正常編譯通過
System.out.print(i);//這句編譯器會提示”非法向前引用”
}
static int i=1;
}
2、與實例構造器中的()方法不同, 它不需要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。 因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object。
3、由於父類的()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類類的變量賦值操作。
4、()方法對於類或者接口來說並不是必須的,如果一個類沒有靜態語句塊,也就沒有變量的賦值操作,那麼編譯器可以不爲這個類生成()方法。
5、接口中不能使用靜態語句塊,但仍然可以有變量初始化的賦值操作,因此接口與類一樣都會生成()方法。但接口與類不同,執行接口的()方法不需要先執行父接口的()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的()方法。
6、虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只有一個線程去執行這個類的()方法,若其中有耗時很長的操作,就可能造成多個線程阻塞。

/*
clinit方法和init方法的區別
1、執行時機不同:
init是對象構造器方法,也就是說在程序執行 new 一個對象調用該對象類的 constructor 方法時纔會執行init方法,而clinit是類構造器方法,也就是在jvm進行類加載—–驗證—-解析—–初始化,中的初始化階段jvm會調用clinit方法。
2、目的不同
init是instance實例構造器,對非靜態變量解析初始化,而clinit是class類構造器對靜態變量,靜態代碼塊進行初始化
init執行的順序是:先初始化成員變量,最後再調用類的構造方法
*/

類加載器:“通過一個類的全額定名來獲取描述此類的二進制字節流”
實現的是類的加載動作;(JVM外部)
對於任何一個類,都需要加載它的類加載器和它本身一同確立它在JVM中的唯一性;每一個類加載器都擁有一個獨立的類名稱空間;兩個類是否相同,必須是在由同一個類加載器加載的前提下(包括equals()方法,instanceof()方法);
不同的類加載器對instanceof關鍵字運算時有影響的;
instanceof:用來在運行時指出對象是否是特定類的一個實例。instanceof通過返回一個布爾值來指出,這個對象是否是這個特定類或者是它的子類的一個實例。result = object instanceof class

雙親委派模型:
從虛擬機的角度來說,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),該類加載器使用C++語言實現,屬於虛擬機自身的一部分。另外一種就是所有其它的類加載器,這些類加載器是由Java語言實現,獨立於JVM外部,並且全部繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,大部分Java程序一般會使用到以下三種系統提供的類加載器:

* 啓動類加載器(Bootstrap classloader):這個類裝載器是在JVM啓動的時候創建的。它負責裝載Java API(java 核心類庫),包含Object對象。和其他的類裝載器不同的地方在於這個裝載器是通過native code來實現的,而不是用Java代碼。負責將存放在\lib 目錄中的、或者是 -Xbootclasspath參數所指定的路徑中的,並且是JVM所識別的類庫加載到JVM內存中;
* 擴展類加載器(Extension classloader):它裝載除了基本的Java API以外的擴展類。它也負責裝載其他的安全擴展功能。\lib\ext
* 系統類加載器(System classloader)或者叫應用程序加載器(Application classloader):如果說bootstrap class loader和extension class loader負責加載的是JVM的組件,那麼system class loader負責加載的是應用程序類。它負責加載用戶在$CLASSPATH裏指定的類。
當然用戶也可以自定類加載器:
* 用戶自定義類加載器(User-defined classloader):這是應用程序開發者用直接用代碼實現的類裝載器。

雙親委派模型的工作過程爲:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啓動類加載器,只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索範圍中沒有找到對應的類)時,子加載器纔會嘗試自己去加載。
使用這種模型來組織類加載器之間的關係的好處是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。增強了穩定性;但是這個模型是非強制性的,是java設計者的推薦模型;

破壞雙親委派模型
第一次:爲了向前兼容
第二次:爲了“調回”,線程上下文加載器
第三次:爲了代碼熱替換,模塊熱部署:
OSGi:實現模塊化熱部署的關鍵在於它自定義的類加載器機制的實現。每一個程序模塊(OSGi中稱之爲Bundle)都有一個自己的類加載器,每次更換Bundle,就把Bundle連同類加載器一起換掉以實現代碼的熱替換;
<*
OSGI
面向Java的動態模型系統
是Java動態化模塊化系統的一系列規範,OSGi服務平臺向Java提供服務,這些服務使Java成爲軟件集成和軟件開發的首選環境。

*>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章