1. 程式人生 > >淺析JVM類載入機制

淺析JVM類載入機制

淺析類載入機制

類載入器簡單來說是用來載入 Java 類到 Java 虛擬機器中的。Java 虛擬機器使用 Java 類的方式如下:Java 源程式(.java 檔案)在經過 Java 編譯器編譯之後就被轉換成 Java 位元組程式碼(.class 檔案)。類載入器負責讀取 Java 位元組程式碼,並轉換成 java.lang.Class類的一個例項。每個這樣的例項用來表示一個 Java 類。通過此例項的 newInstance()方法就可以創建出該類的一個物件。

想要真正深入理解Java類載入機制,就要弄懂三個問題:類什麼時候載入、類載入的過程是什麼、用什麼載入。所以本文分為三部分分別介紹

Java類載入的時機、類載入的過程、載入器。

一、Java類載入的時機

1.1 類載入的生命週期

類載入的生命週期是從類被載入到記憶體開始,直到解除安裝記憶體為止的。整個生命週期分為7個階段:載入、驗證、準備、解析、初始化、使用、解除安裝。其中,驗證、準備、解析三部分統稱為連線。具體步驟如下圖所示:

 

下面簡單介紹下類載入器所執行的生命週期的過程。

(1) 裝載:查詢和匯入Class檔案;

(2) 連結:把類的二進位制資料合併到JRE中;

    (a)校驗:檢查載入Class檔案資料的正確性;

    (b)準備:給類的靜態變數分配儲存空間;

    (c)解析:將符號引用轉成直接引用;

(3) 初始化:對類的靜態變數,靜態程式碼塊執行初始化操作。

1.2 類載入的時機

類載入的時機Java虛擬機器規範中並沒有強制規定,但是對於初始化階段,有5種場景必須立即執行初始化,也被稱為主動引用。

(1) 遇到newgetstaticputstaticinvokestatic4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候,讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。

(2) 使用java.lang.reflect

包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

(3) 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

(4)當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。

(5)當使用JDK 1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStaticREF_putStaticREF_invokeStatic的方法控制代碼,並且該方法控制代碼所對應的類沒有初始化過,則先觸發初始化。

二、Java類載入的過程

類載入的全過程分為7個階段,但是主要的過程是載入、驗證、準備、解析、初始化這5個階段。

2.1 載入

在載入階段,虛擬機器需要完成3件事情:

(1) 通過一個類的全限定名來獲取定義此類的二進位制位元組流;

(2) 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;

(3) Java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。

2.2 驗證

驗證階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。整體來看,驗證階段大致分為4個驗證動作。

(1)檔案格式驗證

第一階段是驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。主要目的是保證輸入的位元組流能正確地解析並存儲於方法區之內,格式上符合描述一個Java型別資訊的要求。該階段是基於二進位制位元組流驗證的,只有通過了這個階段的驗證,位元組流才會進入記憶體的方法去中儲存,後面的3個驗證都是基於方法區的儲存結構進行的。

這一階段可能的驗證點:

a.是否以魔數開頭;

b.主、次版本號是否在當前虛擬機器處理範圍內;

c.常量池的常量資料型別是否被支援;

。。。

(2)元資料驗證

元資料驗證是對位元組碼描述資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。這個階段可能的驗證點:

a.是否有父類;

b.是否繼承了不被允許繼承的類;

c.如果該類不是抽象類,是否實現了其父類或介面要求實現的所有方法;

。。。

(3)位元組碼驗證

位元組碼驗證的主要目的是通過資料流和控制流分析,確定程式語義的合法性和邏輯性。該階段將對類的方法體進行校驗分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的事情。這個階段可能的驗證點:

a.保證任何時候運算元棧的資料型別與指令程式碼序列的一致性;

b.跳轉指令不會跳轉到方法體以外的位元組碼指令上;

。。。

(4)符號引用驗證

符號引用驗證的主要目的是保證解析動作能正常執行,如果無法通過符號引用驗證,則會丟擲異常。這個階段可能的驗證點:

a.符號引用的類、欄位、方法的訪問性(publicprivate等)是否可被當前類訪問;

b.指定類是否存在符合方法的欄位描述符;

。。。

2.3 準備

準備階段是正式為類變數分配並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配,需要說明的是:這時候進行記憶體分配的僅包括類變數(static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中;這裡所說的初始值通常情況是資料型別的零值,例如:

public static int value = 1;

value在準備階段過後的初始值為0而不是1,而把value賦值的putstatic指令將在初始化階段才會被執行。

特殊情況:

public static final int value = 1;//此時準備value賦值為1

2.4 解析

解析階段是虛擬機器將常量池內的符號引用替換成直接引用的過程。直接引用是直接指向目標的指標,相對偏移量或是一個能間接定位到目標的控制代碼。直接引用和虛擬機器實現的記憶體有關,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用不盡相同。

2.5 初始化

初始化階段是類載入過程的最後一步,到了該階段才真正開始執行類定義的Java程式程式碼,根據程式設計師通過程式碼定製的主觀計劃去初始化類變數和其他資源,是執行類構造器初始化方法的過程。


三、類載入器

類載入器大致可以分為以下3部分:

(1) 啟動類載入器: 將存放於<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接引用。

(2) 擴充套件類載入器 : <JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫載入。開發者可以直接使用擴充套件類載入器。

(3) 應用程式類載入器: 負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可直接使用。

我們的應用程式都是由這三種類載入器相互配合載入的。它們的關係如下圖所示,稱之為雙親委派模型。



工作過程:如果一個類載入器接收到了類載入的請求,它首先把這個請求委託給他的父類載入器去完成,每個層次的類載入器都是如此,因此所有的載入請求都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它在搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

好處:java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類載入器要載入這個類,最終都會委派給啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果使用者自己寫了一個名為java.lang.Object的類,並放在程式的Classpath中,那系統中將會出現多個不同的Object類,java型別體系中最基礎的行為也無法保證,應用程式也會變得一片混亂。

雙親委派模型實現起來其實很簡單,以下是實現程式碼,通過以下程式碼,可以對JVM採用的雙親委派類載入機制有了更感性的認識。

//類載入過程

 public Class<?> loadClass(String name) throws ClassNotFoundException {

return loadClass(name, false);

}

 protected synchronized Class<?> loadClass(String name, boolean resolve)

throws ClassNotFoundException

    {

// 首先判斷該型別是否已經被載入

Class c = findLoadedClass(name);

if (c == null) {

//沒有被載入,就委託給父類載入器或者委派給啟動類載入器載入

    try {

if (parent != null) {

// 如果存在父類載入器,就委派給父類載入器載入

c = parent.loadClass(name, false);

} else {

// 如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類

    c = findBootstrapClassOrNull(name);

}

    } catch (ClassNotFoundException e) {

        }

      if (c == null) {

        // 如果父類載入器和啟動類載入器都不能完成載入任務,呼叫自身的載入工程

        c = findClass(name);

    }

}

if (resolve) {

    resolveClass(c);

}

return c;

    }

總結:

本文從Java類載入的時機、類載入的過程以及類載入的方式三方面對Java類載入機制進行了淺析,希望通過閱讀本文可以對Java類載入機制有個大致的瞭解。