類加載機制
虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗、轉換解析、初始化,最終造成可被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。java
在java中,類型的加載、鏈接、初始化過程是在程序運行期間完成的,這種策略雖然會令類加載時稍微增長一些性能開銷,可是會爲java應用程序提供高度的靈活性。
類的整個生命週期包括:加載、鏈接(驗證、準備、解析)、初始化、使用、卸載。類的加載過程必須嚴格的按照這個順序執行,而解析階段不必定:它在某些狀況下能夠在初始化以後運行,這是爲了支持java的運行時綁定。
何時須要開始類加載的第一個階段,java虛擬機規範沒有明確的約束。可是對於初始化階段,虛擬機規範嚴格規定了5種狀況必須對當即對類進行‘初始化’(加載、鏈接要再次以前開始)
- 遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時;
- 使用java.lang.reflect包的方法對類進行反射調用時;
- 當初始化一個類的時候,若是發現其父類還沒初始化時,則須要先觸發父類的初始化;
- 當虛擬機啓動時,用戶須要指定一個要執行的主類,虛擬機會先初始化這個主類;
- 當使用動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果時ref_getstatic、ref_putstatic、ref_invokestatic的方法句柄,而且這個句柄對應的類沒有初始化過,則須要觸發其初始化。
這五種方式,稱爲對類的主動引用,除此以外的全部引用類的方式都不會觸發初始化。
- 經過子類引用父類中的靜態字段只會觸發父類的初始化
- 數組的初始化不會觸發類的初始化階段,會觸發一個由虛擬機自動生成類的初始化,直接繼承Object,由newarray字節碼指令觸發,這個類封裝了數組元素的訪問方法
- 引用類中的static final字段不會觸發類的初始化
- 接口和類的初始化有所不一樣,編譯器會爲接口生成<clinit>類構造器,用戶初始化接口中定義的成員變量。真正的區別時:當一個子接口初始化的時候不會要求初始化其父接口,只有在使用到父接口的時候纔會進行初始化。
類加載第一階段:加載
- 經過一個類的全限定名來獲取次類的二進制字節流
- 將這個二進制流表明的靜態存儲結構轉化爲方法區的運行時數據結構
- 在內存中生成這個類的java.lang.Class對象,做爲方法區中這個類的各類數據訪問入口。
第1條沒有指明這個二進制流要從一個class文件中獲取,在此基礎上創建了許多舉足輕重的技術:
- 從ZIP包中讀取,最終成爲jar,ear,war格式的基礎;
- 從網絡中獲取,典型的應用就是Applet;
- 運行時計算生成,最多的就是動態代理技術;
- 從其餘文件中獲取,如jsp應用,有jsp文件生成對應的class類;
- 從數據庫中讀取,相對少見
相對於類加載過程當中其餘階段,一個非數組類的加載階段(準確的說,時加載階段中獲取二進制流的動做)是開發人員可控性最強的,由於加載階段既可使用系統提供的引導類加載器來完成,也能夠由用戶自定義的類加載器去完成,開發人員能夠經過定義本身的類加載器去控制字節流的拉取方式(即重寫一個類加載器的loadClass方法)。
數組類不須要經過類加載器建立,由java虛擬機直接建立,可是數組的類型元素最終是要經過類加載器建立,一個數組類的建立過程要遵循如下規則:
- 若是數組的的組件類型是引用類型(如Integer[]),那就遞歸採用類的加載過程去加載這個組件類型,數組C將在加載該組件類型的類加載器的類名稱空間上被標識。
- 若是數組的組件類型不是引用類型(如int[]),java虛擬機會把類標記爲與引導類加載器關聯。
- 數組類的可見性與它的組件類型的可見性一致,若是組件類型不是引用類型,那數組的可見性將默認爲public。
對於hotSpot虛擬機而言,class對象比較特殊,雖然是對象,可是卻存放在方法區中(hotSpot虛擬機的方法區其實也在堆中,稱爲永久代)數據庫
類加載第二階段:鏈接(驗證、準備、解析)
加載階段與鏈接階段的部份內容(如一部分字節碼文件的格式驗證動做)多是交叉進行的,加載階段未結束,鏈接階段可能已經開始。
- 驗證是鏈接階段的第一步,這一階段的目的是爲了確保class文件的字節流中包含的信息符合當前虛擬機的要求,並不會危害虛擬機自身的安全。總體上看,驗證階段大體上會分紅4個階段的驗證動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
- 準備階段是正式爲類變量分配內存並設置類變量初始值的階段, 這些變量使用的內存最終都會在方法區中分配。
- 解析階段時虛擬機將常量池中的符號引用替換爲直接引用的過程。
符號引用:符號引用一組符號來描述要引用的目標,符號引用能夠是任何形式的字面量,只要能無歧義的定位到目標便可。
直接引用:直接引用能夠是直接執行目標的指針、相對偏移量或句柄。若是有了直接引用那麼內存中必定存在該對象。
虛擬機規範中並未規定解析階段發生的具體時間,因此虛擬機實現能夠根據須要來判斷是在類被加載器加載時就對常量池中的符號引用進行解析,仍是等到一個符號引用將要被使用前纔去解析。
除invokedynamic指令外,虛擬機實現能夠對第一次解析的結果進行緩存(在運行時常量池記錄直接引用,並把符號引用標識爲已解析狀態),若是成功一直成功,若是失敗一直失敗。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符。
類加載第三階段:初始化
初始化:類初始化階段是加載過程的最後一步,真正開始執行類中定義的java程序代碼。
初始化階段是執行類構造器<clinit>() 方法的過程。
- <clinit>()是編譯器自動收集類變量的賦值動做和靜態代碼塊中語句合併產生的,收集順序按照代碼出現的順序,在前面的靜態代碼塊能夠賦值不能訪問。
- <clinit>()不用顯示調用父類構造器,虛擬機會保證在子類構造器調用以前,調用父類構造器,所以在虛擬機中第一個被執行的是Object類構造器。
- 因爲父類<clinit>()方法先調用,意味着父類中的靜態代碼塊優先於子類。
- <clinit>()方法不是必需的。
- 接口能夠存在賦值語句,也會生成<clinit>()方法,接口初始化時父接口中的<clinit>()方法不會被執行,接口的實現類初始化時也不會執行接口的<clinit>()方法。
- 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖和同步,多個線程去初始化一個類,只有會一個線程去執行<clinit>()方法。
類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」是在java虛擬機外部實現的,實現這個動做的代碼模塊稱爲「類加載器」。
對於任意一個類,都須要由加載它的加載器和這個類自己一同確立其在java虛擬機中的惟一性,每個類加載器都有其獨立的命名空間。
雙親委派模型:從java虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器,這個類加載器使用C++語言實現,時虛擬機自身的一部分;另外一種就是全部其餘類的類加載器,這些加載器都由java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類ClassLoader。從開發人員的角度可劃分:
啓動類加載器、擴展類加載器、應用程序類加載器、自定義類加載器。
雙親委派模型要求除了頂層的啓動類加載器外,其他的類都應有本身的父類加載器(非強制要求)。
工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器,最終請求傳送到最頂層的啓動類加載器,若是父類在它的搜索範圍內沒有找到所需的類,反饋本身沒法加載這個類,子類纔會本身嘗試加載這個類。好處是java類隨着它的類加載具有了一種帶優先級的層次關係。