JVM類加載機制

1、概述

Java編譯器編譯好.class文件之後,我們需要使用JVM來運行這個class文件。那麼最開始的工作就是要把字節碼從磁盤輸入到內存中,這個過程我們叫做加載。加載完成之後,我們就可以進行一系列的運行前準備工作了,比如:爲類靜態變量開闢空間,將常量池存放在方法區內存中並實現常量池地址解析,初始化類靜態變量等等。

java類的生命週期

指一個class文件從加載到卸載的全過程。一個java類的完整的生命週期會經歷

加載->鏈接(驗證+準備+解析)->初始化(使用前的準備)->使用->卸載  五個階段

1、加載:查找並加載類的二進制數據 
2、連接 
    –驗證:確保被加載的類的正確性 
    –準備:爲類的靜態變量分配內存,並將其初始化爲默認值 
    –解析:把類中的符號引用轉換爲直接引用 
3、初始化:爲類的靜態變量賦予正確的初始值 
     從上邊我們可以看出類的靜態變量賦了兩回值。這是爲什麼呢?原因是,在連接過程中時爲靜態變量賦值爲默認值,也就是說,只要是你定義了靜態變量,不管你開始給沒給它設置,我係統都爲他初始化一個默認值。到了初始化過程,系統就檢查是否用戶定義靜態變量時有沒有給設置初始化值,如果有就把靜態變量設置爲用戶自己設置的初始化值,如果沒有還是讓靜態變量爲初始化值。

2、加載

2.1 jvm加載類過程

當我們使用命令來執行某一個Java程序(比如Test.class)的時候:java Test
(1) java.exe 會幫助我們找到JRE ,接着找到位於JRE內部的 jvm.dll ,這纔是真正的Java虛擬機器 , 最後加載動態庫,激活Java虛擬機器
(2) 虛擬機器激活以後,會先做一些初始化的動作,比如說讀取系統參數等。一旦初始化動作完成之後,就會產生第一個類裝載器 ―― Bootstrap Loader(啓動類裝載器 ) 。
(3) Bootstrap Loader所做的初始工作中,除了一些基本的初始化動作之外,最重要的就是加載 Launcher.java 之中的 ExtClassLoader(擴展類裝載器) ,並設定其 Parent爲null,代表其父加載器爲 BootstrapLoader 。
(4) 然後Bootstrap Loader再要求加載 Launcher.java之中的 AppClassLoader(用戶自定義類裝載器 ) ,並設定其 Parent爲之前產生的 ExtClassLoader 實體。這兩個加載器都是以靜態類的形式存在的。
這裏要請大家注意的是,Launcher$ExtClassLoader.class 與 Launcher$AppClassLoader.class 都是由Bootstrap Loader所加載,所以Parent和由哪個類加載器加載沒有關係。
 

2.2類加載器體系結構

JVM加載class文件必須通過一個叫做類裝載器的程序,它的作用就是從磁盤文件中將要運行代碼的字節碼流加載進內存(JVM管理的方法區)中。


(1).BootStrap ClassLoader:啓動類加載器,負責加載存放在%JAVA_HOME%\lib目錄中的,或者通被-Xbootclasspath參數所指定的路徑中的,並且被java虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫,即使放在指定路徑中也不會被加載)類庫到虛擬機的內存中,啓動類加載器無法被java程序直接引用。
(2).Extension ClassLoader:擴展類加載器,由sun.misc.Launcher$ExtClassLoader實現,負責加載%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
(3).Application ClassLoader:應用程序類加載器,由sun.misc.Launcher$AppClassLoader實現,負責加載用戶類路徑classpath上所指定的類庫,是類加載器ClassLoader中的getSystemClassLoader()方法的返回值,開發者可以直接使用應用程序類加載器,如果程序中沒有自定義過類加載器,該加載器就是程序中默認的類加載器。
注意:上述三個JDK提供的類加載器雖然是父子類加載器關係,但是沒有使用繼承,而是使用了組合關係。
從JDK1.2開始,java虛擬機規範推薦開發者使用雙親委派模式(ParentsDelegation Model)進行類加載,其加載過程如下:
(1).如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器去完成。
(2).每一層的類加載器都把類加載請求委派給父類加載器,直到所有的類加載請求都應該傳遞給頂層的啓動類加載器。
(3).如果頂層的啓動類加載器無法完成加載請求,子類加載器嘗試去加載,如果連最初發起類加載請求的類加載器也無法完成加載請求時,將會拋出ClassNotFoundException,而不再調用其子類加載器去進行類加載。


2.3類加載器的加載順序


JVM並不是把所有的類一次性全部加載到JVM中的,也不是每次用到一個類的時候都去查找,對於JVM級別的類加載器在啓動時就會把默認的JAVA_HOME/lib裏的class文件加載到JVM中,因爲這些是系統常用的類,對於其他的第三方類,則採用用到時就去找,找到了就緩存起來的,下次再用到這個類的時候就可以直接用緩存起來的類對象了。


2.4 加載階段完成任務

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


3、連接

類被加載後,就進入連接階段。連接就是將已經讀入到內存的類的二進制數據合併到虛擬機的運行時環境中去。

3.1驗證

驗證:當一個類被加載之後,必須要驗證一下這個類是否合法,即是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,比如這個類是不是符合字節碼的格式、變量與方法是不是有重複、數據類型是不是有效、繼承與實現是否合乎標準等等。總之,這個階段的目的就是保證加載的類是能夠被jvm所運行。很多人都感覺,既然這個類都通過編譯加載到內存裏了,那肯定就是合法的了,爲什麼還要驗證呢,這是因爲這裏的驗證時爲了避免有人惡意編寫class文件,也就是說並不是通過編譯得到的class文件。所以這裏驗證其實是檢查的class文件的內部結構是否符合字節碼的要求。


檢驗主要經歷幾個步驟:文件格式驗證->元數據驗證->字節碼驗證->符號引用驗證

文件格式驗證:驗證字節流是否符合Class文件格式的規範並 驗證其版本是否能被當前的jvm版本所處理。ok沒問題後,字節流就可以進入內存的方法區進行保存了。

後面的3個校驗都是在方法區進行的。

元數據驗證:對字節碼描述的信息進行語義化分析,保證其描述的內容符合java語言的語法規範。

字節碼檢驗:最複雜,對方法體的內容進行檢驗,保證其在運行時不會作出什麼出格的事來。

符號引用驗證:來驗證一些引用的真實性與可行性,比如代碼裏面引了其他類,這裏就要去檢測一下那些來究竟是否存在;或者說代碼中訪問了其他類的一些屬性,這裏就對那些屬性的可以訪問行進行了檢驗。(這一步將爲後面的解析工作打下基礎

3.2準備
準備:準備階段的工作就是爲類的靜態變量分配內存並設爲jvm默認的初值這些變量所使用的內存在方法區中進行分配,對於非靜態的變量,則不會爲它們分配內存。有一點需要注意,這時候,靜態變量的初值爲jvm默認的初值,而不是我們在程序中設定的初值。jvm默認的初值是這樣的:
基本類型(int、long、short、char、byte、boolean、float、double)的默認值爲0。
引用類型的默認值爲null。
常量的默認值爲我們程序中設定的值,比如我們在程序中定義final static int a = 100,則準備階段中a的初值就是100。

3.3解析
解析:這一階段的任務就是把常量池中的符號引用轉換爲直接引用

那麼什麼是符號引用,什麼又是直接引用呢?

我們來舉個例子:我們要找一個人,我們現有的信息是這個人的身份證號是1234567890。只有這個信息我們顯然找不到這個人,但是通過公安局的身份系統,我們輸入1234567890這個號之後,就會得到它的全部信息:比如山東省濱州市濱城區18號張三,通過這個信息我們就能找到這個人了。這裏,123456790就好比是一個符號引用,而山東省濱州市濱城區18號張三就是直接引用。在內存中也是一樣,比如我們要在內存中找一個類裏面的一個叫做show的方法,顯然是找不到。但是在解析階段,jvm就會把show這個名字轉換爲指向方法區的的一塊內存地址,比如c17164,通過c17164就可以找到show這個方法具體分配在內存的哪一個區域了。這裏show就是符號引用,而c17164就是直接引用。在解析階段,jvm會將所有的類或接口名、字段名、方法名轉換爲具體的內存地址

 

連接階段完成之後會根據使用的情況(直接引用還是被動引用)來選擇是否對類進行初始化。


4、初始化
如果一個類被直接引用,就會觸發類的初始化。在java中,直接引用的情況有:

什麼時候要對類進行初始化工作(加載+鏈接在此之前已經完成了),jvm有嚴格的規定(五種情況):
1.遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時,假如類還沒進行初始化,則馬上對其進行初始化工作。其實就是3種情況:用new實例化一個類時、讀取或者設置類的靜態字段時(不包括被final修飾的靜態字段,因爲他們已經被塞進常量池了)、以及執行靜態方法的時候。
2.使用java.lang.reflect.*的方法對類進行反射調用的時候,如果類還沒有進行過初始化,馬上對其進行。
3.初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。
4.當jvm啓動時,用戶需要指定一個要執行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。
5.用Class.forName(String className);來加載類的時候,也會執行初始化動作。注意:ClassLoader的loadClass(String className);方法只會加載並編譯某類,並不會對其執行初始化。

類的初始化過程是這樣的:按照順序自上而下運行類中的變量賦值語句和靜態語句,如果有父類,則首先按照順序運行父類中的變量賦值語句和靜態語句。


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