深入理解JVM的類加載


前言:

  前面又說到Java程序實際上是將。class文件放入JVM中運行。虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換,解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是JVM的類加載機制

一、類加載的過程

  類從加載虛擬機內存中開始到卸載出內存為止,生命周期包括:加載、驗證、准備、解析、初始化、使用、卸載。

1、加載

  通過一個類的全限定名來獲取定義此類的二進制字節流(沒有指明二進制字節流要從一個Class文件中獲取,可以從ZIP包中讀取,從網絡中獲取,運行時計算生成等等)將這個字節流所代表的靜態儲存結構轉化為方法區的運行時數據結構在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口完成后,虛擬機外部的二進制字節流就按照虛擬機所需格式儲存在方法區中。這里稍微理解一下對象和類的概念,對象是實例化的類。類的信息是存儲在方法區中的,對象是存儲在Java堆中的。類是對象的模板,對象是類的實例。這里主要講類。

2、驗證

  加載階段未完成,連接階段已經開始。兩者之間會交叉運行。因為Class文件可以用任何途徑產生,字節流不符合Class文件格式的約束,虛擬機會拋出java.lang.VerifyError異常,所以為了保護虛擬機,JVM會有以下四個方面的驗證

  文件格式驗證:即驗證類文件結構

  元數據驗證:這個是否有父類,父類是否繼承了不允許被繼承的類等等  

  字節碼驗證:對類的方法體進行校驗,JDK1.6后只需檢查StackMapTable屬性中的記錄是否合法,JDK1.7后對於主版本號大於50的Class文件,使用類型檢查來完成數據流分析

  符號引用驗證:全限定名是否能找到對應的類,在指定類中是否存在符合方法的字段描述以及簡單名稱描述的方法,字段。訪問性是否正確。驗證不成功會拋出java.lang.incompatibleClassChangeError異常的子類。

3、准備

  正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個時候進行內存分配的只包括類變量(被static修飾的變量),並不包括實例變量,實例變量是在對象實例化時隨對象一起分配在java堆中。這時候分配的初始值為零值,假設一個變量定義為:public static int value = 123;則設置變量的初始值應該為0, 而不是123。 把value賦值為123的putstatic指令是在程序被編譯后,存放於類構造器<clinit>()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。如果字段屬性表中存在ConstantValue屬性,那么在准備階段會將value賦值為ConstantValue屬性所指定的值 。例如:public static final int value = 123; 那么在准備階段,則會將value賦值為123;

4、解析

  將符號引用轉換為直接引用的過程。符號引用是一組以符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用是能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標並不一定已經加載到內存中。而直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。

  解析動作主要針對:類或接口、字段(類成員變量)、類方法、接口方法等引用進行。 

  類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是對普通的對象類型的引用,從而進行不同的解析。

  字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關系從上往下遞歸搜索其父類,直至查找結束,查找流程如下圖所示:

 

  最后需要注意:理論上是按照上述順序進行搜索解析,但在實際應用中,虛擬機的編譯器實現可能要比上述規范要求的更嚴格一些。如果有一個同名字段同時出現在該類的接口和父類中,或同時在自己或父類的接口中出現,編譯器可能會拒絕編譯。

  類方法解析:對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。

  接口方法解析:與類方法解析步驟類似,由於接口不會有父類,因此,只遞歸向上搜索父接口就行了。

5、初始化

  是類加載過程的最后一步,到了此階段,才真正開始執行類中定義的Java程序代碼。在准備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序指定的主觀計划去初始化類變量和其他資源。到初始化階段,才真正開始執行類中的Java程序代碼。即初始化階段是執行類構造器<clinit>()方法的過程。 

  <clinit>()方法解析過程:<clinit>方法是有編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合並產生的,編譯器的順序是由語句在源文件中出現的順序決定的,所以靜態語句塊中只能訪問到定義在靜態語句塊之前的變量定義在它之后的變量,在前面的靜態語句塊可以賦值,但不能訪問。虛擬機會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢,因此在虛擬機中的一個被執行的<clinit>()方法的類肯定是java。lang。Object。<clinit>()方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法所以這個<clinit>()方法主要是給靜態變量賦值和執行靜態語句塊。接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法,但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。

  虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能會造成多個進程阻塞,在實際應用中這種阻塞往往是很隱蔽的。

小結:

  目前來說也就是加載階段獲取類的信息,驗證階段驗證類是否符合JVM的標准。這兩個階段可以交叉運行。然后就需要在准備階段給類分配內存空間,設置常量的初始值,初始化靜態變量等等。解析階段就是將符號引用轉換為直接引用,讓類在JVM上有對應的內存。最后是初始化階段就是執行類中的靜態變量賦值語句和靜態語句塊。到這里的類一些基礎的工作已經做好了,可以使用了。

二、Java中的類加載器與雙親委派機制

  實現“通過一個類的權限定名來獲取描述此類的二進制字節流”這個動作的代碼模塊稱為類加載器java類的加載是由虛擬機來完成的,虛擬機把描述類的Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成能被java虛擬機直接使用的java類型,這就是虛擬機的類加載機制。JVM中用來完成上述功能的具體實現就是類加載器。類加載器讀取。class字節碼文件將其轉換成java。lang。Class類的一個實例。每個實例用來表示一個java類。通過該實例的newInstance()方法可以創建出一個該類的對象。

1、引導類加載器(Bootstrap ClassLoader)

  這個類加載器負責將<JAVA_HOME>\lib目錄下的類庫加載到虛擬機內存中,用來加載java的核心庫,此類加載器並不繼承於java.lang.ClassLoader,不能被java程序直接調用,代碼是使用C++編寫的。是虛擬機自身的一部分

2、擴展類加載器(Extendsion ClassLoader)

  這個類加載器負責加載<JAVA_HOME>\lib\ext目錄下的類庫,用來加載java的擴展庫,開發者可以直接使用這個類加載器。

3、應用程序類加載器(Application ClassLoader)

  這個類加載器負責加載用戶類路徑(CLASSPATH)下的類庫,一般我們編寫的java類都是由這個類加載器加載,這個類加載器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也稱為系統類加載器。一般情況下這就是系統默認的類加載器

4、自定義的類加載器

  這個加載器可以滿足我們加載類的特殊需求,需要繼承java.lang.ClassLoader類並且覆蓋其中的findClass()方法和defineClass()方法。

5、雙親委派機制

  上面的4個加載器並不是並行加載的,JVM中是通過雙親委派機制來加載的:

  雙親委派模型是一種組織類加載器之間關系的一種規范,它的工作原理是:如果一個類加載器收到了類加載的請求,它不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,這樣層層遞進,最終所有的加載請求都被傳到最頂層的啟動類加載器中,只有當父類加載器無法完成這個加載請求(它的搜索范圍內沒有找到所需的類)時,才會交給子類加載器去嘗試加載。這樣的好處是:java類隨着它的類加載器一起具備了帶有優先級的層次關系。這是十分必要的,比如java.lang.Object,它存放在\jre\lib\rt。jar中,它是所有java類的父類,因此無論哪個類加載都要加載這個類,最終所有的加載請求都匯總到頂層的啟動類加載器中。Object類會由啟動類加載器來加載,所以加載的都是同一個類,如果不使用雙親委派模型,由各個類加載器自行去加載的話,系統中就會出現不止一個Object類,應用程序就會全亂了。

三、必須對類進行初始化的5種情況

  1、遇到new,getstatic,putstatic,invokestatic字節碼指令。最常見的Java代碼場景是:使用new實例化對象、讀取或設置一個類的靜態字段(被final修飾或已在編譯器把結果放入常量池的靜態字段除外)、調用一個類的靜態方法

  2、使用java.lang.reflect包的方法對類進行反射調用

  3、類繼承了父類,父類要先初始化

  4、虛擬機啟動,初始化主類

  5、當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM