在代碼編譯後,就會生成JVM(Java虛擬機)可以識別的二進制字節流文件(*.class)。而JVM把Class文件中的類描述數據從文件加載到內存,並對數據進行校驗、轉換解析、初始化,使這些數據最終成爲能夠被JVM直接使用的Java類型,這個說來簡單但實際複雜的過程叫作JVM的類加載機制。java
先來查看一波代碼數組
package com.black.example.helloworld; public class JvmTest { public static void main(String[] args) { ClassLoader classLoader = JvmTest.class.getClassLoader(); while (classLoader!=null){ System.out.println(classLoader); classLoader = classLoader.getParent(); } } }運行結果:安全
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@7530d0a從上面的結果能夠看出,並無獲取到ExtClassLoader的父Loader,緣由是Bootstrap Loader(引導類加載器)是用C語言實現的,找不到一個肯定的返回父Loader的方式,因而就返回null。數據結構
當一個類加載器收到一個類加載的請求,它首先會將該請求委派給父類加載器去加載,每個層次的類加載器都是如此,所以全部的類加載請求最終都應該被傳入到頂層的啓動類加載器(Bootstrap ClassLoader)中,只有當父類加載器反饋沒法完成這個列的加載請求時(它的搜索範圍內不存在這個類),子類加載器才嘗試加載。其層次結構示意圖以下:app
加載順序:自頂向下
檢查順序:自底向上學習雙親委任的好處是:
- 避免重複加載,父類加載過了,子類就不須要再加載了。
- 是一種安全機制,解決了各個類加載器的基礎類的統一問題。程序安全是jdk的事,文件安全是系統的事
示例以下:測試
package java.util; //能編譯,可是不能運行 public class List { public static void main(String[] args) { System.out.println("我是List"); } }運行結果:this
錯誤: 在類 java.util.List 中找不到 main 方法, 請將 main 方法定義爲: public static void main(String[] args) 不然 JavaFX 應用程序類必須擴展javafx.application.Application雙親委派的底層實現,源代碼以下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先先檢查該類已經被加載過了 Class<?> c = findLoadedClass(name); if (c == null) {//該類沒有加載過,交給父類加載 long t0 = System.nanoTime(); try { if (parent != null) {//交給父類加載 c = parent.loadClass(name, false); } else {//父類不存在,則交給啓動類加載器加載 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //父類加載器拋出異常,沒法完成類加載請求 } if (c == null) { //父類加載器沒法完成類加載請求時,調用自身的findClass方法來完成類加載 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
- 經過命令行啓動應用時由JVM初始化加載含有main()方法的主類。
- 經過Class.forName()方法動態加載,會默認執行初始化塊(static{}),可是Class.forName(name,initialize,loader)中的initialze可指定是否要執行初始化塊。
- 經過ClassLoader.loadClass()方法動態加載,不會執行初始化塊。
一、遵照雙親委派模型:繼承ClassLoader,重寫findClass()方法。
二、破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。 spa一般咱們推薦採用第一種方法自定義類加載器,最大程度上的遵照雙親委派模型。 .net
5.1 加載
加載過程主要完成三件事情:
- 經過類的全限定名來獲取定義此類的二進制字節流
- 將這個類字節流表明的靜態存儲結構轉爲方法區的運行時數據結構
- 在堆中生成一個表明此類的java.lang.Class對象,做爲訪問方法區這些數據結構的入口。
這個過程主要就是類加載器完成。(對於HotSpot虛擬而言,Class對象較爲特殊,其被放置在方法區而不是堆中)
5.2 連接之驗證
此階段主要確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機的自身安全。主要包括如下四個階段:
- 文件格式驗證:基於字節流驗證,驗證字節流符合當前的Class文件格式的規範,能被當前虛擬機處理。驗證經過後,字節流纔會進入內存的方法區進行存儲。好比版本號不一致的問題
- 元數據驗證:基於方法區的存儲結構驗證,對字節碼進行語義驗證,確保不存在不符合java語言規範的元數據信息。
- 字節碼驗證:基於方法區的存儲結構驗證,經過對數據流和控制流的分析,保證被檢驗類的方法在運行時不會作出危害虛擬機的動做。
- 符號引用驗證:基於方法區的存儲結構驗證,發生在解析階段,確保可以將符號引用成功的解析爲直接引用,其目的是確保解析動做正常執行。換句話說就是對類自身之外的信息進行匹配性校驗。
5.2 連接之準備
準備要執行的指定類,準備階段爲變量分配內存並設置靜態變量初始化
爲類的靜態變量(static filed)在方法區分配內存,並賦默認初值(0值或null值)。如static int a = 100;
靜態變量a就會在準備階段被賦默認值0。
對於通常的成員變量是在類實例化時候,隨對象一塊兒分配在堆內存中。
另外,靜態常量(static final filed)會在準備階段賦程序設定的初值,如static final int a = 666; 靜態常量a就會在準備階段被直接賦值爲666,對於靜態變量,這個操做是在初始化階段進行的。
5.2 解析:將類的二進制數據中的符號引用換爲直接引用。
5.3 初始化
類初始化是類加載的最後一步,除了加載階段,用戶能夠經過自定義的類加載器參與,其餘階段都徹底由虛擬機主導和控制。到了初始化階段才真正執行Java代碼。
類的初始化的主要工做是爲靜態變量賦程序設定的初值。
如static int a = 100;在準備階段,a被賦默認值0,在初始化階段就會被賦值爲100。Java虛擬機規範中嚴格規定了有且只有五種狀況必須對類進行初始化:
一、使用new字節碼指令建立類的實例,或者使用getstatic、putstatic讀取或設置一個靜態字段的值(放入常量池中的常量除外),或者調用一個靜態方法的時候,對應類必須進行過初始化。
二、經過java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則要首先進行初始化。
三、當初始化一個類的時候,若是發現其父類沒有進行過初始化,則首先觸發父類初始化。
四、當虛擬機啓動時,用戶須要指定一個主類(包含main()方法的類),虛擬機會首先初始化這個類。
五、使用jdk1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、 RE_invokeStatic的方法句柄,而且這個方法句柄對應的類沒有進行初始化,則須要先觸發其初始化。注意,虛擬機規範使用了「有且只有」這個詞描述,這五種狀況被稱爲「主動引用」,除了這五種狀況,全部其餘的類引用方式都不會觸發類初始化,被稱爲「被動引用」。
被動引用的例子一:
經過子類引用父類的靜態字段,對於父類屬於「主動引用」的第一種狀況,對於子類,沒有符合「主動引用」的狀況,故子類不會進行初始化。代碼以下:
//父類 public class SuperClass { //靜態變量value public static int value = 666; //靜態塊,父類初始化時會調用 static{ System.out.println("父類初始化!"); } } //子類 public class SubClass extends SuperClass{ //靜態塊,子類初始化時會調用 static{ System.out.println("子類初始化!"); } } //主類、測試類 public class NotInit { public static void main(String[] args){ System.out.println(SubClass.value); } } //####輸出結果: //####父類初始化! //####666被動引用的例子之二:
經過數組來引用類,不會觸發類的初始化,由於是數組new,而類沒有被new,因此沒有觸發任何「主動引用」條款,屬於「被動引用」。代碼以下:
//主類、測試類 class NotInit { public static void main(String[] args){ SuperClass[] test = new SuperClass[10]; } } 輸出結果:沒有任何輸出被動引用的例子之三:
剛剛講解時也提到,靜態常量在編譯階段就會被存入調用類的常量池中,不會引用到定義常量的類,這是一個特例,須要特別記憶,不會觸發類的初始化!
package com.black.example.helloworld; //常量類 public class ConstClass { static{ System.out.println("常量類初始化!"); } public static final String HELLOWORLD = "hello world!"; } //主類、測試類 class NotInit1 { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } } 輸出結果:hello world!
下一篇:JVM學習筆記二:內存結構規範