一、概述
虛擬機(jī)的類加載機(jī)制定義:把描述類的數(shù)據(jù)從Class文件(一串二進(jìn)制的字節(jié)流)加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、轉(zhuǎn)換解析和初始化,最終形成被虛擬機(jī)直接使用的Java類型。
在Java語言里,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成的,Java里天生可以動態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動態(tài)加載和動態(tài)連接這個特點(diǎn)實現(xiàn)的。
用戶可以通過Java預(yù)定義的和自定義類加載器,讓一個本地的應(yīng)用程序可以在運(yùn)行時從網(wǎng)絡(luò)或其他地方加載一個二進(jìn)制流作為程序代碼的一部分。
二、類加載的時機(jī)
2.1 類加載包含那些階段
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存,所經(jīng)過的生命周期有:
- 1.加載
- 2.驗證
- 3.準(zhǔn)備
- 4.解析
- 5.初始化
- 6.使用
- 7.卸載
其中2-4統(tǒng)稱為連接,上面的過程有幾個需要注意的點(diǎn):
- 加載、驗證、準(zhǔn)備、初始化、卸載這五個階段按順序按部就班地開始,在一個階段執(zhí)行的過程中有可能調(diào)用、激活另外一個階段。
- 解析階段有可能在初始化之后開始,這是為了支持
Java語言的運(yùn)行時綁定。
2.2 類加載觸發(fā)的時機(jī)
有且僅有下面五種情況必須立即對類進(jìn)行初始化:
- 第一種:遇到
new/getstatic/putstatic/invokestatic這4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化,場景:- 使用
new關(guān)鍵字實例化對象 - 讀取或設(shè)置一個類的靜態(tài)字段(被
final修飾,已在編譯期把結(jié)果放入常量池的字段除外) - 調(diào)用一個類的靜態(tài)方法
- 使用
//1.new關(guān)鍵字.
LoadInvokeClass loadInvokeClass = new LoadInvokeClass();
//2.訪問靜態(tài)變量
int content = LoadInvokeClass.sContent;
//3.調(diào)用靜態(tài)方法.
LoadInvokeClass.staticMethod();
- 第二種:使用
java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
try {
Class<?> mClass = Class.forName("com.example.lizejun.repojavalearn.load.LoadInvokeClass");
} catch (Exception e) { e.printStackTrace(); }
- 第三種:當(dāng)初始化一個類的時候,如果需要初始化其父類,但是發(fā)現(xiàn)父類沒有初始化、那么需要先觸發(fā)其父類的初始化。
//其中LoadInvokeClass是LoadInvokeClassChild的父類.
LoadInvokeClassChild classChild = new LoadInvokeClassChild();
- 第四種:當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含
main()方法),虛擬機(jī)會先初始化這個主類。 - 第五種:使用
JDK 1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic/REF_putStatic/REF_invokeStatic的句柄方法,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
2.3 被動引用
在2.2中談到的都是主動引用,除此之外,所有引用類的方法都稱為被動引用,而被動引用不會觸發(fā)類的初始化:
- 類初始化時,如果父類沒有被初始化,那么會先初始化父類,這一過程將一直遞歸到
Object為止,但是不會去初始化它所實現(xiàn)的接口,即當(dāng)我們初始化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");
}
}
- 接口初始化時,不要求父接口全部初始化,只有真正用到了父接口的時候(如引用接口中定義的常量),那么才會初始化。
- 當(dāng)訪問某個類的靜態(tài)域時,不會觸發(fā)父類的初始化或者子類的初始化,即使靜態(tài)域被子類或子接口或者它的實現(xiàn)類所引用,我們給
ClassChild添加一個靜態(tài)屬性,訪問這個靜態(tài)屬性不會初始化ClassParent。
public class ClassChild extends ClassParent {
public static int sNumber;
static {
System.out.println("load ClassChild");
}
}
- 如果一個靜態(tài)變量是編譯時常量,則對它的引用不會引起定義它的類的初始化,如下面訪問
sNumber,那么不會引起ClassChild的實例化。
public class ClassChild extends ClassParent {
public static final int sNumber = 2;
static {
System.out.println("load ClassChild");
}
}
- 通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化。
ClassChild[] children = new ClassChild[10];
三、類加載的過程
3.1 加載
在"加載"階段,虛擬機(jī)需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的
java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
3.2 驗證
"驗證"階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害自身的安全,大致會完成下面四個階段的校驗動作:
- 文件格式驗證
- 元數(shù)據(jù)驗證
- 字節(jié)碼驗證
- 符號引用驗證
3.3 準(zhǔn)備
"準(zhǔn)備"階段是正式為類變量(被static修飾,而不是實例變量)分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。
- 對于
static并且非final的類變量,將被初始化為數(shù)據(jù)類型的零值。 - 對于
static且final的類變量,在這個階段就會被初始化為ConstantValue屬性所指定的值。
3.4 解析
“解析”階段是虛擬機(jī)將常量池的符號引用替換為直接引用的過程,包括:
- 類或接口的解析
- 字段解析
- 類方法解析
- 接口方法解析
3.5 初始化
根據(jù)程序員通過程序指定的主觀計劃去初始化類變量和其它資源,也就是執(zhí)行類構(gòu)造器<clinit>()方法的過程:
<clinit>方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并而成,順序是由語句在源文件中出現(xiàn)的順序決定的。靜態(tài)語句塊只能訪問到定義在它之前的變量,對于定義在它后面的變量只能賦值不能訪問。<clinit>()方法與類的構(gòu)造函數(shù)不同,它不需要顯示地調(diào)用父類構(gòu)造器,虛擬機(jī)會保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢,因此在虛擬機(jī)中第一個杯知行的<clinit>()方法的類肯定是java.lang.Object。父類的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。
如果一個類中沒有靜態(tài)語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成
<clinit>()方法。接口不能接口中僅有變量初始化的賦值操作,但執(zhí)行接口的
<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法,只有當(dāng)父接口中定義的變量使用時,父接口才會初始化,另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法。虛擬機(jī)會保證一個類的
<clinit>()方法在多線程環(huán)境中被正確地加鎖、同步。
四、類加載器
4.1 概念
類加載器用來“通過一個類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”。
4.2 類與類加載器
類加載器用于實現(xiàn)類的加載動作,除此之外,任意一個類,都需要由它加載它的類加載器和這個類本身一同確立其在Java虛擬機(jī)中的唯一性。
每一個類加載器,都擁有一個獨(dú)立的類名稱空間,比較兩個類是否相等,只有在兩個類由同一個類加載器加載的前提下才有意義。
相等代表類的Class對象的equals方法,isAssignableFrom方法,isInstance方法。
4.3 雙親委派模型
絕大部分Java程序都會用到以下三種系統(tǒng)提供的類加載器:
- 啟動類加載器
- 擴(kuò)展類加載器
- 應(yīng)用類加載器
類加載器之間的層次關(guān)系,稱為類加載器的雙親委派模型,這個模型要求除了頂層的啟動類加載器外,其余的類都應(yīng)當(dāng)有自己的父類加載器,一般使用組合來復(fù)用父加載器的代碼。
雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,只有當(dāng)父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己加載。
五、對象實例化
在類加載過程完畢后,如果需要進(jìn)行實例化對象就需要經(jīng)過一下步驟,按優(yōu)先加載父類,再到子類的順序執(zhí)行:
- 加載父類構(gòu)造器
- 為父類實例對象分配存儲空間并賦值
- 執(zhí)行父類的初始化塊
- 執(zhí)行父類構(gòu)造函數(shù)
- 加載子類加載器
- 為子類實例對象分配存儲控件并賦值
- 執(zhí)行子類的初始化塊
- 執(zhí)行子類構(gòu)造函數(shù)
我們用一個簡單的例子:
其中ClassOther是一個單獨(dú)的類:
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");
}
}
當(dāng)我們實例化一個ClassChild對象時,調(diào)用的順序如下:
