一、概述
虛擬機器的類載入機制定義:把描述類的資料從Class
檔案(一串二進位制的位元組流)載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成被虛擬機器直接使用的Java
型別。
在Java
語言裡,型別的載入、連線和初始化過程都是在程式執行期間完成的,Java
裡天生可以動態擴充套件的語言特性就是依賴執行期動態載入和動態連線這個特點實現的。
使用者可以通過Java
預定義的和自定義類載入器,讓一個本地的應用程式可以在執行時從網路或其他地方載入一個二進位制流作為程式程式碼的一部分。
二、類載入的時機
2.1 類載入包含那些階段
類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體,所經過的生命週期有:
- 1.載入
- 2.驗證
- 3.準備
- 4.解析
- 5.初始化
- 6.使用
- 7.解除安裝
其中2-4
統稱為連線,上面的過程有幾個需要注意的點:
- 載入、驗證、準備、初始化、解除安裝這五個階段按順序按部就班地開始,在一個階段執行的過程中有可能呼叫、啟用另外一個階段。
- 解析階段有可能在初始化之後開始,這是為了支援
Java
語言的執行時繫結。
2.2 類載入觸發的時機
有且僅有下面五種情況必須立即對類進行初始化:
- 第一種:遇到
new/getstatic/putstatic/invokestatic
這4
條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化,場景: - 使用
new
關鍵字例項化物件 - 讀取或設定一個類的靜態欄位(被
final
修飾,已在編譯期把結果放入常量池的欄位除外) - 呼叫一個類的靜態方法
//1.new關鍵字.
LoadInvokeClass loadInvokeClass = new LoadInvokeClass();
//2.訪問靜態變數
int content = LoadInvokeClass.sContent;
//3.呼叫靜態方法.
LoadInvokeClass.staticMethod();
複製程式碼
- 第二種:使用
java.lang.reflect
包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
try {
Class<?> mClass = Class.forName("com.example.lizejun.repojavalearn.load.LoadInvokeClass");
} catch (Exception e) { e.printStackTrace(); }
複製程式碼
- 第三種:當初始化一個類的時候,如果需要初始化其父類,但是發現父類沒有初始化、那麼需要先觸發其父類的初始化。
//其中LoadInvokeClass是LoadInvokeClassChild的父類.
LoadInvokeClassChild classChild = new LoadInvokeClassChild();
複製程式碼
- 第四種:當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含
main()
方法),虛擬機器會先初始化這個主類。 - 第五種:使用
JDK 1.7
的動態語言支援時,如果一個java.lang.invoke.MethodHandle
例項最後的解析結果REF_getStatic/REF_putStatic/REF_invokeStatic
的控制程式碼方法,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。
2.3 被動引用
在2.2
中談到的都是主動引用,除此之外,所有引用類的方法都稱為被動引用,而被動引用不會觸發類的初始化:
- 類初始化時,如果父類沒有被初始化,那麼會先初始化父類,這一過程將一直遞迴到
Object
為止,但是不會去初始化它所實現的介面,即當我們初始化ClassChild
的時候,只會先初始化ClassParent
,淡不會初始化ClassInterface
。
public interface ClassInterface {}
public class ClassParent implements ClassInterface {
static {
System.out.println("load ClassParent");
}
}
public class ClassChild extends ClassParent {
static {
System.out.println("load ClassChild");
}
}
複製程式碼
- 介面初始化時,不要求父介面全部初始化,只有真正用到了父介面的時候(如引用介面中定義的常量),那麼才會初始化。
- 當訪問某個類的靜態域時,不會觸發父類的初始化或者子類的初始化,即使靜態域被子類或子介面或者它的實現類所引用,我們給
ClassChild
新增一個靜態屬性,訪問這個靜態屬性不會初始化ClassParent
。
public class ClassChild extends ClassParent {
public static int sNumber;
static {
System.out.println("load ClassChild");
}
}
複製程式碼
- 如果一個靜態變數是編譯時常量,則對它的引用不會引起定義它的類的初始化,如下面訪問
sNumber
,那麼不會引起ClassChild
的例項化。
public class ClassChild extends ClassParent {
public static final int sNumber = 2;
static {
System.out.println("load ClassChild");
}
}
複製程式碼
- 通過陣列定義來引用類,不會觸發此類的初始化。
ClassChild[] children = new ClassChild[10];
複製程式碼
三、類載入的過程
3.1 載入
在"載入"階段,虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在記憶體中生成一個代表這個類的
java.lang.Class
物件,作為方法區這個類的各種資料的訪問入口。
3.2 驗證
"驗證"階段的目的是為了確保Class
檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害自身的安全,大致會完成下面四個階段的校驗動作:
- 檔案格式驗證
- 後設資料驗證
- 位元組碼驗證
- 符號引用驗證
3.3 準備
"準備"階段是正式為類變數(被static
修飾,而不是例項變數)分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。
- 對於
static
並且非final
的類變數,將被初始化為資料型別的零值。 - 對於
static
且final
的類變數,在這個階段就會被初始化為ConstantValue
屬性所指定的值。
3.4 解析
“解析”階段是虛擬機器將常量池的符號引用替換為直接引用的過程,包括:
- 類或介面的解析
- 欄位解析
- 類方法解析
- 介面方法解析
3.5 初始化
根據程式設計師通過程式指定的主觀計劃去初始化類變數和其它資源,也就是執行類構造器<clinit>()
方法的過程:
-
<clinit>
方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併而成,順序是由語句在原始檔中出現的順序決定的。靜態語句塊只能訪問到定義在它之前的變數,對於定義在它後面的變數只能賦值不能訪問。 -
<clinit>()
方法與類的建構函式不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()
方法執行前,父類的<clinit>()
方法已經執行完畢,因此在虛擬機器中第一個杯知行的<clinit>()
方法的類肯定是java.lang.Object
。 -
父類的靜態語句塊要優先於子類的變數賦值操作。
-
如果一個類中沒有靜態語句塊,也沒有對類變數的賦值操作,那麼編譯器可以不為這個類生成
<clinit>()
方法。 -
介面不能介面中僅有變數初始化的賦值操作,但執行介面的
<clinit>()
方法不需要先執行父介面的<clinit>()
方法,只有當父介面中定義的變數使用時,父介面才會初始化,另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()
方法。 -
虛擬機器會保證一個類的
<clinit>()
方法在多執行緒環境中被正確地加鎖、同步。
四、類載入器
4.1 概念
類載入器用來“通過一個類的全限定名來獲取描述此類的二進位制位元組流”。
4.2 類與類載入器
類載入器用於實現類的載入動作,除此之外,任意一個類,都需要由它載入它的類載入器和這個類本身一同確立其在Java
虛擬機器中的唯一性。
每一個類載入器,都擁有一個獨立的類名稱空間,比較兩個類是否相等,只有在兩個類由同一個類載入器載入的前提下才有意義。
相等代表類的Class
物件的equals
方法,isAssignableFrom
方法,isInstance
方法。
4.3 雙親委派模型
絕大部分Java
程式都會用到以下三種系統提供的類載入器:
- 啟動類載入器
- 擴充套件類載入器
- 應用類載入器
類載入器之間的層次關係,稱為類載入器的雙親委派模型,這個模型要求除了頂層的啟動類載入器外,其餘的類都應當有自己的父類載入器,一般使用組合來複用父載入器的程式碼。
雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,它首先不會去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,只有當父類載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己載入。
五、物件例項化
在類載入過程完畢後,如果需要進行例項化物件就需要經過一下步驟,按優先載入父類,再到子類的順序執行:
- 載入父類構造器
- 為父類例項物件分配儲存空間並賦值
- 執行父類的初始化塊
- 執行父類建構函式
- 載入子類載入器
- 為子類例項物件分配儲存控制元件並賦值
- 執行子類的初始化塊
- 執行子類建構函式
我們用一個簡單的例子:
其中ClassOther
是一個單獨的類:
public class ClassOther {
public int mNumber;
public ClassOther() {
System.out.println("ClassOther Constructor");
}
public void setNumber(int number) {
this.mNumber = number;
}
public int getNumber() {
return mNumber;
}
}
複製程式碼
ClassChild
則繼承於ClassChild
:
public class ClassParent {
{
System.out.println("ClassParent before mClassParentContent");
}
private ClassOther mClassParentContent = new ClassOther(10);
{
System.out.println("ClassParent after mClassParentContent=" + mClassParentContent.mNumber);
}
public ClassParent(int number) {
mClassParentContent.setNumber(number);
System.out.println("ClassParent Constructor, mClassParentContent=" + mClassParentContent.mNumber);
}
}
public class ClassChild extends ClassParent {
{
System.out.println("ClassChild before a");
}
private int mClassChildContent = 1;
{
System.out.println("ClassChild after mClassChildContent=" + mClassChildContent);
}
public ClassChild() {
super(2);
System.out.println("ClassChild Constructor");
}
}
複製程式碼
當我們例項化一個ClassChild
物件時,呼叫的順序如下: