JVM類加載過程共分爲加載、驗證、準備、解析、初始化、使用和卸載七個階段
這些階段一般都是互相交叉的混合式進行的,一般會在一個階段執行的過程當中調用或激活另一個階段。java
加載過程是JVM類加載的第一步,若是JVM配置中打開-XX:+TraceClassLoading
,咱們能夠在控制檯觀察到相似數據庫
[Loaded chapter7.SubClass from file:/E:/EclipseData-Mine/Jvm/build/classes/]
的輸出,這就是類加載過程的日誌。
加載過程是做爲程序猿最可控的一個階段,由於你能夠隨意指定類加載器,甚至能夠重寫loadClass
方法,固然,在jdk1.2及之後的版本中,loadClass
方法是包含雙親委派模型的邏輯代碼的,因此不建議重寫這個方法,而是鼓勵重寫findClass
方法。
類加載的二進制字節碼文件能夠來自jar
包、網絡、數據庫以及各類語言的編譯器編譯而來的.class
文件等各類來源。
加載過程主要完成以下三件工做:
1>經過類的全限定名(包名+類名)來獲取定義此類的二進制字節流
2>將字節流所表明的靜態存儲結構轉化爲運行時數據結構存儲在方法區
3>爲類生成java.lang.Class
對象,並做爲該類的惟一入口segmentfault
這裏涉及到一個概念就是類的惟一性,書上對該概念的解釋是:在類的加載過程當中,一個類由類加載器和類自己惟一肯定。也就是說,若是一個JVM虛擬機中有多個不一樣加載器,即便他們加載同一個類文件,那獲得的java.lang.Class
對象也是不一樣的。所以,只有在同一個加載器中,一個類才能被惟一標識,這叫作類加載器隔離。數組
驗證過程相對來講就有複雜一點了,不過驗證過程對JVM的安全仍是相當重要的,畢竟你不知道比人的代碼究竟能幹出些什麼。
驗證過程主要包含四個驗證過程:
1>文件格式驗證
四個驗證過程當中,只有格式驗證是創建在二進制字節流的基礎上的。格式驗證就是對文件是不是0xCAFEBABE
開頭、class文件版本等信息進行驗證,確保其符合JVM虛擬機規範。
2>元數據驗證
元數據驗證是對源碼語義分析的過程,驗證的是子類繼承的父類是不是final類;若是這個類的父類是抽象類,是否實現了起父類或接口中要求實現的全部方法;子父類中的字段、方法是否產生衝突等,這個過程把類、字段和方法看作組成類的一個個元數據,而後根據JVM規範,對這些元數據之間的關係進行驗證。因此,元數據驗證階段並未深刻到方法體內。
3>字節碼驗證
既然元數據驗證並未深刻到方法體內部,那麼到了字節碼驗證過程,這一步就不可避免了。字節碼主要是對方法體內部的代碼的先後邏輯、關係的校驗,例如:字節碼是否執行到了方法體之外、類型轉換是否合理等。
固然,這很複雜。
因此,即便是到了現在jdk1.8,也仍是沒法徹底保證字節碼驗證準確無遺漏的。並且,若是在字節碼驗證浪費了大量的資源,彷佛也有些得不償失。
4>符號引用驗證
符號引用的驗證實際上是發生在符號引用向直接引用轉化的過程當中,而這一過程發生在解析階段。
由於都是驗證,因此一併在這講。符號引用驗證作的工做主要是驗證字段、類方法以及接口方法的訪問權限、根據類的全限定名是否能定位到該類等。具體過程會在接下來的解析階段進行分析。
好了,驗證階段的工做基本就是以上四類,下面咱們來看下一個階段。安全
相信經歷過艱辛的驗證階段的磨練,JVM和咱們都倍感疲憊。因此,接下來的準備階段給咱們提供了一個相對輕鬆的休息階段。
準備階段要作的工做很簡單,他瞄準了類變量這個元數據,把他放進了方法區並進行了初始化,這裏的初始化並非<init>
或者<clinit>
操做,準備階段只是將這些可愛的類變量置零。網絡
這一部分我畫了幾個圖,內容有些多,放在另外一篇文章裏:解析數據結構
初始化階段是咱們能夠大搞實驗的一塊實驗田。首先,初始化階段作什麼?這個階段就是執行<clinit>
方法。而<clinit>
方法是由編譯器按照源碼順序依次掃描類變量的賦值動做和static
代碼塊獲得的。
那麼問題來了,啥時候纔會觸發一個類的初始化的操做呢?答案有且只有五個:
1>在類沒有進行過初始化的前提下,當執行new
、getStatic
、setStatic
、invokeStatic
字節碼指令時,類會當即初始化。對應的java操做就是new
一個對象、讀取/寫入一個類變量(非final
類型)或者執行靜態方法。
2>在類沒有進行過初始化的前提下,當一個類的子類被初始化以前,該父類會當即初始化。
3>在類沒有進行過初始化的前提下,當包含main
方法時,該類會第一個初始化。
4>在類沒有進行過初始化的前提下,當使用java.lang.reflect
包的方法對類進行反射調用時,該類會當即初始化。
5>在類沒有進行過初始化的前提下,當使用JDK1.5
支持時,若是一個java.langl.incoke.MethodHandle
實例最後的解析結果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
以上五種狀況被稱做類的五種主動引用,除此以外的任何狀況都被相應地叫作被動引用。如下是集中常見的且容易迷惑人心智的被動引用的示例:併發
/** 經過子類引用父類的類變量不會觸發子類的初始化操做 */ public class SuperClass { public static String value = "superClass value"; static { System.out.println("SuperClass init!"); } } public class SubClass extends SuperClass implements SuperInter{ static { System.out.println("SubClass init!"); } } public class InitTest { static { System.out.println("InitTest init!");//main第一個初始化 } public static void main(String[] args) { System.out.println(SubClass.value); } } /** output: InitTest init! SuperClass init! superClass value */
/** 經過定義對象數組的方式是不能觸發對象初始化的 */ public static void main(String[] args) { SubClass[] superArr = new SubClass[10]; } /** output: InitTest init! */
/** 引用類的final類型的類變量沒法觸發類的初始化操做 */ public class SuperClass { public static final String CONSTANT_STRING = "constant"; static { System.out.println("SuperClass init!"); } } public class InitTest { static { System.out.println("InitTest init!");//main } public static void main(String[] args) { System.out.println(SuperClass.CONSTANT_STRING);//getStatic } } /** output: InitTest init! constant */
瞭解了何時出發初始化操做後,那麼初始化操做的執行順序是什麼樣的?併發初始化狀況下的運行機制又如何?
JVM虛擬機規定了幾條標準:ide
static
代碼塊,那就不生成<clinit>
方法.<clinit>
方法不須要先執行父接口的<clinit>
方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>
方法。<clinit>
方法的執行具備同步性,而且只執行一次。但當一個線程執行該類的<clinit>
方法時,其餘的初始化線程需阻塞等待。咱們經過一個實例來驗證線程的阻塞問題:ui
public class SuperClass { static { System.out.println("SuperClass init!"); System.out.println("Thread.currentThread(): " + Thread.currentThread() + " excuting..."); try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } } } public class InitTest { static { System.out.println("InitTest init!");//main } public static void main(String[] args) throws ClassNotFoundException, InterruptedException { currentInitTest(); } public static void currentInitTest() throws InterruptedException { Runnable run = new Runnable() { @Override public void run() { System.out.println("Thread.currentThread(): " + Thread.currentThread() + " start"); new SuperClass(); System.out.println("Thread.currentThread(): " + Thread.currentThread() + " end"); } }; Thread[] threadArr = new Thread[10]; for (int i = 0; i < 10; i++) { threadArr[i] = new Thread(run); } for (Thread thread : threadArr) { thread.start(); } } } /** output: InitTest init! Thread.currentThread(): Thread[Thread-0,5,main] start Thread.currentThread(): Thread[Thread-1,5,main] start Thread.currentThread(): Thread[Thread-2,5,main] start Thread.currentThread(): Thread[Thread-7,5,main] start Thread.currentThread(): Thread[Thread-6,5,main] start Thread.currentThread(): Thread[Thread-3,5,main] start Thread.currentThread(): Thread[Thread-5,5,main] start Thread.currentThread(): Thread[Thread-9,5,main] start Thread.currentThread(): Thread[Thread-4,5,main] start Thread.currentThread(): Thread[Thread-8,5,main] start SuperClass init! Thread.currentThread(): Thread[Thread-0,5,main] excuting... Thread.currentThread(): Thread[Thread-9,5,main] end Thread.currentThread(): Thread[Thread-3,5,main] end Thread.currentThread(): Thread[Thread-6,5,main] end Thread.currentThread(): Thread[Thread-7,5,main] end Thread.currentThread(): Thread[Thread-0,5,main] end Thread.currentThread(): Thread[Thread-5,5,main] end Thread.currentThread(): Thread[Thread-4,5,main] end Thread.currentThread(): Thread[Thread-8,5,main] end Thread.currentThread(): Thread[Thread-1,5,main] end Thread.currentThread(): Thread[Thread-2,5,main] end */