JVM類載入過程分析及驗證

NO IMAGE

JVM類載入過程共分為載入、驗證、準備、解析、初始化、使用和解除安裝七個階段

這些階段通常都是互相交叉的混合式進行的,通常會在一個階段執行的過程中呼叫或啟用另外一個階段。

載入

載入過程是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物件,並作為該類的唯一入口

這裡涉及到一個概念就是類的唯一性,書上對該概念的解釋是:在類的載入過程中,一個類由類載入器和類本身唯一確定。也就是說,如果一個JVM虛擬機器中有多個不同載入器,即使他們載入同一個類檔案,那得到的java.lang.Class物件也是不同的。因此,只有在同一個載入器中,一個類才能被唯一標識,這叫做類載入器隔離。

驗證

驗證過程相對來說就有複雜一點了,不過驗證過程對JVM的安全還是至關重要的,畢竟你不知道比人的程式碼究竟能幹出些什麼。
驗證過程主要包含四個驗證過程:
1>檔案格式驗證
四個驗證過程中,只有格式驗證是建立在二進位制位元組流的基礎上的。格式驗證就是對檔案是否是0xCAFEBABE開頭、class檔案版本等資訊進行驗證,確保其符合JVM虛擬機器規範。
2>後設資料驗證
後設資料驗證是對原始碼語義分析的過程,驗證的是子類繼承的父類是否是final類;如果這個類的父類是抽象類,是否實現了起父類或介面中要求實現的所有方法;子父類中的欄位、方法是否產生衝突等,這個過程把類、欄位和方法看做組成類的一個個後設資料,然後根據JVM規範,對這些後設資料之間的關係進行驗證。所以,後設資料驗證階段並未深入到方法體內。
3>位元組碼驗證
既然後設資料驗證並未深入到方法體內部,那麼到了位元組碼驗證過程,這一步就不可避免了。位元組碼主要是對方法體內部的程式碼的前後邏輯、關係的校驗,例如:位元組碼是否執行到了方法體以外、型別轉換是否合理等。
當然,這很複雜。
所以,即使是到了如今jdk1.8,也還是無法完全保證位元組碼驗證準確無遺漏的。而且,如果在位元組碼驗證浪費了大量的資源,似乎也有些得不償失。
4>符號引用驗證
符號引用的驗證其實是發生在符號引用向直接引用轉化的過程中,而這一過程發生在解析階段。
因為都是驗證,所以一併在這講。符號引用驗證做的工作主要是驗證欄位、類方法以及介面方法的訪問許可權、根據類的全限定名是否能定位到該類等。具體過程會在接下來的解析階段進行分析。
好了,驗證階段的工作基本就是以上四類,下面我們來看下一個階段。

準備

相信經歷過艱辛的驗證階段的磨練,JVM和我們都倍感疲憊。所以,接下來的準備階段給我們提供了一個相對輕鬆的休息階段。
準備階段要做的工作很簡單,他瞄準了類變數這個後設資料,把他放進了方法區並進行了初始化,這裡的初始化並不是<init>或者<clinit>操作,準備階段只是將這些可愛的類變數置零。

解析

這一部分我畫了幾個圖,內容有些多,放在另一篇文章裡:解析

初始化

初始化階段是我們可以大搞實驗的一塊實驗田。首先,初始化階段做什麼?這個階段就是執行<clinit>方法。而<clinit>方法是由編譯器按照原始碼順序依次掃描類變數的賦值動作和static程式碼塊得到的。
那麼問題來了,啥時候才會觸發一個類的初始化的操作呢?答案有且只有五個:
1>在類沒有進行過初始化的前提下,當執行newgetStaticsetStaticinvokeStatic位元組碼指令時,類會立即初始化。對應的java操作就是new一個物件、讀取/寫入一個類變數(非final型別)或者執行靜態方法。
2>在類沒有進行過初始化的前提下,當一個類的子類被初始化之前,該父類會立即初始化。
3>在類沒有進行過初始化的前提下,當包含main方法時,該類會第一個初始化。
4>在類沒有進行過初始化的前提下,當使用java.lang.reflect包的方法對類進行反射呼叫時,該類會立即初始化。
5>在類沒有進行過初始化的前提下,當使用JDK1.5支援時,如果一個java.langl.incoke.MethodHandle例項最後的解析結果REF_getStaticREF_putStaticREF_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虛擬機器規定了幾條標準:

  • 先父類後子類,(原始碼中)先出現先執行
  • 向前引用:一個類變數在定義前可以賦值,但是不能訪問。
  • 非必須:如果一個類或介面沒有類變數的賦值動作和static程式碼塊,那就不生成<clinit>方法.
  • 執行介面的<clinit>方法不需要先執行父介面的<clinit>方法。只有當父介面中定義的變數被使用時,父介面才會被初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>方法。
  • 同步性:<clinit>方法的執行具有同步性,並且只執行一次。但當一個執行緒執行該類的<clinit>方法時,其他的初始化執行緒需阻塞等待。

我們通過一個例項來驗證執行緒的阻塞問題:

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
*/