一扔傅、概述
虛擬機的類加載機制定義:把描述類的數(shù)據(jù)從Class
文件(一串二進制的字節(jié)流)加載到內(nèi)存,并對數(shù)據(jù)進行校驗诡蜓、轉(zhuǎn)換解析和初始化熬甫,最終形成被虛擬機直接使用的Java
類型。
在Java
語言里万牺,類型的加載罗珍、連接和初始化過程都是在程序運行期間完成的,Java
里天生可以動態(tài)擴展的語言特性就是依賴運行期動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的脚粟。
用戶可以通過Java
預(yù)定義的和自定義類加載器覆旱,讓一個本地的應(yīng)用程序可以在運行時從網(wǎng)絡(luò)或其他地方加載一個二進制流作為程序代碼的一部分。
二核无、類加載的時機
2.1 類加載包含那些階段
類從被加載到虛擬機內(nèi)存中開始扣唱,到卸載出內(nèi)存,所經(jīng)過的生命周期有:
- 1.加載
- 2.驗證
- 3.準備
- 4.解析
- 5.初始化
- 6.使用
- 7.卸載
其中2-4
統(tǒng)稱為連接团南,上面的過程有幾個需要注意的點:
- 加載噪沙、驗證、準備吐根、初始化正歼、卸載這五個階段按順序按部就班地開始,在一個階段執(zhí)行的過程中有可能調(diào)用拷橘、激活另外一個階段局义。
- 解析階段有可能在初始化之后開始,這是為了支持
Java
語言的運行時綁定冗疮。
2.2 類加載觸發(fā)的時機
有且僅有下面五種情況必須立即對類進行初始化:
- 第一種:遇到
new/getstatic/putstatic/invokestatic
這4
條字節(jié)碼指令時萄唇,如果類沒有進行過初始化,則需要先觸發(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
包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化诅挑,則需要先觸發(fā)其初始化四敞。
try {
Class<?> mClass = Class.forName("com.example.lizejun.repojavalearn.load.LoadInvokeClass");
} catch (Exception e) { e.printStackTrace(); }
- 第三種:當初始化一個類的時候,如果需要初始化其父類拔妥,但是發(fā)現(xiàn)父類沒有初始化忿危、那么需要先觸發(fā)其父類的初始化。
//其中LoadInvokeClass是LoadInvokeClassChild的父類.
LoadInvokeClassChild classChild = new LoadInvokeClassChild();
- 第四種:當虛擬機啟動時毒嫡,用戶需要指定一個要執(zhí)行的主類(包含
main()
方法),虛擬機會先初始化這個主類。 - 第五種:使用
JDK 1.7
的動態(tài)語言支持時兜畸,如果一個java.lang.invoke.MethodHandle
實例最后的解析結(jié)果REF_getStatic/REF_putStatic/REF_invokeStatic
的句柄方法努释,并且這個方法句柄所對應(yīng)的類沒有進行過初始化,則需要先觸發(fā)其初始化咬摇。
2.3 被動引用
在2.2
中談到的都是主動引用伐蒂,除此之外,所有引用類的方法都稱為被動引用肛鹏,而被動引用不會觸發(fā)類的初始化:
- 類初始化時逸邦,如果父類沒有被初始化,那么會先初始化父類在扰,這一過程將一直遞歸到
Object
為止缕减,但是不會去初始化它所實現(xiàn)的接口,即當我們初始化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");
}
}
- 接口初始化時皱卓,不要求父接口全部初始化裹芝,只有真正用到了父接口的時候(如引用接口中定義的常量),那么才會初始化娜汁。
- 當訪問某個類的靜態(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 加載
在"加載"階段彼绷,虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)茴迁。
- 在內(nèi)存中生成一個代表這個類的
java.lang.Class
對象寄悯,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
3.2 驗證
"驗證"階段的目的是為了確保Class
文件的字節(jié)流中包含的信息符合當前虛擬機的要求堕义,并且不會危害自身的安全猜旬,大致會完成下面四個階段的校驗動作:
- 文件格式驗證
- 元數(shù)據(jù)驗證
- 字節(jié)碼驗證
- 符號引用驗證
3.3 準備
"準備"階段是正式為類變量(被static
修飾,而不是實例變量)分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配洒擦。
- 對于
static
并且非final
的類變量椿争,將被初始化為數(shù)據(jù)類型的零值。 - 對于
static
且final
的類變量熟嫩,在這個階段就會被初始化為ConstantValue
屬性所指定的值秦踪。
3.4 解析
“解析”階段是虛擬機將常量池的符號引用替換為直接引用的過程,包括:
- 類或接口的解析
- 字段解析
- 類方法解析
- 接口方法解析
3.5 初始化
根據(jù)程序員通過程序指定的主觀計劃去初始化類變量和其它資源掸茅,也就是執(zhí)行類構(gòu)造器<clinit>()
方法的過程:
<clinit>
方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并而成椅邓,順序是由語句在源文件中出現(xiàn)的順序決定的。靜態(tài)語句塊只能訪問到定義在它之前的變量昧狮,對于定義在它后面的變量只能賦值不能訪問景馁。<clinit>()
方法與類的構(gòu)造函數(shù)不同,它不需要顯示地調(diào)用父類構(gòu)造器陵且,虛擬機會保證在子類的<clinit>()
方法執(zhí)行前裁僧,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢,因此在虛擬機中第一個杯知行的<clinit>()
方法的類肯定是java.lang.Object
慕购。父類的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作聊疲。
如果一個類中沒有靜態(tài)語句塊,也沒有對類變量的賦值操作沪悲,那么編譯器可以不為這個類生成
<clinit>()
方法获洲。接口不能接口中僅有變量初始化的賦值操作,但執(zhí)行接口的
<clinit>()
方法不需要先執(zhí)行父接口的<clinit>()
方法殿如,只有當父接口中定義的變量使用時贡珊,父接口才會初始化,另外涉馁,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()
方法门岔。虛擬機會保證一個類的
<clinit>()
方法在多線程環(huán)境中被正確地加鎖、同步烤送。
四寒随、類加載器
4.1 概念
類加載器用來“通過一個類的全限定名來獲取描述此類的二進制字節(jié)流”。
4.2 類與類加載器
類加載器用于實現(xiàn)類的加載動作帮坚,除此之外妻往,任意一個類,都需要由它加載它的類加載器和這個類本身一同確立其在Java
虛擬機中的唯一性试和。
每一個類加載器讯泣,都擁有一個獨立的類名稱空間,比較兩個類是否相等阅悍,只有在兩個類由同一個類加載器加載的前提下才有意義好渠。
相等代表類的Class
對象的equals
方法昨稼,isAssignableFrom
方法,isInstance
方法拳锚。
4.3 雙親委派模型
絕大部分Java
程序都會用到以下三種系統(tǒng)提供的類加載器:
- 啟動類加載器
- 擴展類加載器
- 應(yīng)用類加載器
類加載器之間的層次關(guān)系悦昵,稱為類加載器的雙親委派模型,這個模型要求除了頂層的啟動類加載器外晌畅,其余的類都應(yīng)當有自己的父類加載器,一般使用組合來復(fù)用父加載器的代碼寡痰。
雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求抗楔,它首先不會去嘗試加載這個類,而是把這個請求委派給父類加載器去完成拦坠,只有當父類加載器反饋自己無法完成這個加載請求時连躏,子加載器才會嘗試自己加載。
五贞滨、對象實例化
在類加載過程完畢后入热,如果需要進行實例化對象就需要經(jīng)過一下步驟,按優(yōu)先加載父類晓铆,再到子類的順序執(zhí)行:
- 加載父類構(gòu)造器
- 為父類實例對象分配存儲空間并賦值
- 執(zhí)行父類的初始化塊
- 執(zhí)行父類構(gòu)造函數(shù)
- 加載子類加載器
- 為子類實例對象分配存儲控件并賦值
- 執(zhí)行子類的初始化塊
- 執(zhí)行子類構(gòu)造函數(shù)
我們用一個簡單的例子:
其中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
對象時勺良,調(diào)用的順序如下: