一起學Java虛擬機系列:
前言
了解JVM是對Java程序員的基本要求蟆沫,但是有多少同學和我有一樣醉心解bug堆布局冗疮,忘記了內(nèi)功修煉咳秉,對JVM的理解是零碎的。系統(tǒng)地學習一次JVM也許能讓我們在這條路走得更好更遠七咧。
類的生命周期
一個類型從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)悠夯、驗證(Verification)寸谜、準備(Preparation)竟稳、解析(Resolution)、初始化(Initialization)熊痴、使用(Using)和卸載(Unloading)七個階段他爸,其中驗證、準備果善、解析三個部分統(tǒng)稱為連接(Linking)诊笤。
類加載的過程
加載
“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段,在加載階段巾陕,Java虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流讨跟。
- 將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據(jù)結構。
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象鄙煤,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
加載階段結束后晾匠,Java虛擬機外部的二進制字節(jié)流就按照虛擬機所設定的格式存儲在方法區(qū)之中
了,方法區(qū)中的數(shù)據(jù)存儲格式完全由虛擬機實現(xiàn)自行定義梯刚,《Java虛擬機規(guī)范》未規(guī)定此區(qū)域的具體
數(shù)據(jù)結構凉馆。類型數(shù)據(jù)妥善安置在方法區(qū)之后,會在Java堆內(nèi)存中實例化一個java.lang.Class類的對象亡资,
這個對象將作為程序訪問方法區(qū)中的類型數(shù)據(jù)的外部接口
驗證
驗證是連接階段的第一步句喜,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機規(guī)范》的全部約束要求,保證這些信息被當作代碼運行后不會危害虛擬機自身的安全沟于。2011年《Java虛擬機規(guī)范(Java SE 7版)》出版咳胃,規(guī)范中大幅增加了驗證過程的描述,驗證階段大致上會完成下面四個階段的檢驗動作:文件格式驗證旷太、元數(shù)據(jù)驗證展懈、字節(jié)碼驗證和符號引用驗證。
1. 文件格式驗證
第一階段要驗證字節(jié)流是否符合Class文件格式的規(guī)范供璧,并且能被當前版本的虛擬機處理存崖。
2. 元數(shù)據(jù)驗證
第二階段的主要目的是對類的元數(shù)據(jù)信息進行語義校驗,保證不存在與《Java語言規(guī)范》定義相悖的元數(shù)據(jù)信息睡毒。
3. 字節(jié)碼驗證
第三階段是整個驗證過程中最復雜的一個階段来惧,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定程序語義是合法的演顾、符合邏輯的供搀。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗完畢以后隅居,這階段就要對類的方法體(Class文件中的Code屬性)進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為葛虐。
4. 符號引用驗證
最后一個階段的校驗行為發(fā)生在虛擬機將符號引用轉化為直接引用[3]的時候胎源,這個轉化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的各類信息進行匹配性校驗屿脐,通俗來說就是涕蚤,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法的诵、字段等資源万栅。
準備
準備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設置類變量初始值的階段西疤。
關于準備階段申钩,還有兩個容易產(chǎn)生混淆的概念筆者需要著重強調,首先是實現(xiàn)這時候進行內(nèi)存分配的僅包括類變量瘪阁,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中邮偎。其次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值管跺。
public static int value = 123;
例如value在準備階段過后的初始值為0,而不是123禾进。而把value賦值為123的putstatic指令是程序被編譯后豁跑,存放于類構造器<clinit>()方法之中,所以把value賦值為123的動作要到類的初始化階段才會被執(zhí)行泻云。
public static final int value = 123;
對于ConstantValue屬性艇拍,那在準備階段變量值就會被初始化為ConstantValue屬性所指定的初始值。
解析
解析階段是Java虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程宠纯。
1. 符號引用(Symbolic References):
符號引用以一組符號來描述所引用的目標卸夕,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可婆瓜。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關快集,引用的目標并不一定是已經(jīng)加載到虛擬機內(nèi)存當中的內(nèi)容。各種虛擬機實現(xiàn)的內(nèi)存布局可以各不相同廉白,但是它們能接受的符號引用必須都是一致的个初,因為實現(xiàn)符號引用的字面量形式明確定義在《Java虛擬機規(guī)范》的Class文件格式中。
2. 直接引用(Direct References):
直接引用是可以直接指向目標的指針猴蹂、相對偏移量或者是一個能間接定位到目標的句柄院溺。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局直接相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同磅轻。如果有了直接引用珍逸,那引用的目標必定已經(jīng)在虛擬機的內(nèi)存中存在逐虚。
初始化
類的初始化階段是類加載過程的最后一個步驟,之前介紹的幾個類加載的動作里弄息,除了在加載階段用戶應用程序可以通過自定義類加載器的方式局部參與外痊班,其余動作都完全由Java虛擬機來主導控制。直到初始化階段摹量,Java虛擬機才真正開始執(zhí)行類中編寫的Java程序代碼涤伐,將主導權移交給應用程序。
進行準備階段時缨称,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值凝果,而在初始化階段,則會根據(jù)程序員通過程序編碼制定的主觀計劃去初始化類變量和其他資源睦尽。我們也可以從另外一種更直接的形式來表達:初始化階段就是執(zhí)行類構造器<clinit>()
方法的過程器净。<clinit>()
并不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動生成物当凡。
-
<clinit>()
方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的山害,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量沿量,定義在它之后的變量浪慌,在前面的靜態(tài)語句塊可以賦值,但是不能訪問.
public class Test {
static {實現(xiàn)
i = 0; // 給變量復制可以正常編譯通過
System.out.print(i); // 這句編譯器會提示“非法向前引用”
}
static int i = 1;
}
-
<clinit>()
方法與類的構造函數(shù)(即在虛擬機視角中的實例構造器<init>()方法)不同朴则,它不需要顯式地調用父類構造器权纤,Java虛擬機會保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢乌妒。因此在Java虛擬機中第一個被執(zhí)行的<clinit>()方法的類型肯定是java.lang.Object汹想。 - 由于父類的
<clinit>()
方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作撤蚊。
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);//輸出 2
}
-
<clinit>()
方法對于類或接口來說并不是必需的古掏,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作侦啸,那么編譯器可以不為這個類生成<clinit>()
方法冗茸。 - 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作匹中,因此接口與類一樣都會生成
<clinit>()
方法夏漱。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法顶捷,因為只有當父接口中定義的變量被使用時挂绰,父接口才會被初始化。此外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法葵蒂。 - Java虛擬機必須保證一個類的<clinit>()方法在多線程環(huán)境中被正確地加鎖同步交播,如果多個線程同時去初始化一個類,那么只會有其中一個線程去執(zhí)行這個類的<clinit>()方法践付,其他線程都需要阻塞等待秦士,直到活動線程執(zhí)行完畢<clinit>()方法。如果在一個類的<clinit>()方法中有耗時很長的操作永高,那就可能造成多個進程阻塞隧土,在實際應用中這種阻塞往往是很隱蔽的
static class DeadLoopClass {
static {
// 如果不加上這個if語句,編譯器將提示“Initializer does not complete normally”并拒絕編譯
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
類加載器
比較兩個類是否“相等”命爬,只有在這兩個類是由同一個類加載器加載的前提下才有意義曹傀,否則,即使這兩個類來源于同一個Class文件饲宛,被同一個Java虛擬機加載皆愉,只要加載它們的類加載器不同,那這兩個類就必定不相等艇抠。
雙親委派模型
1. 啟動類加載器(Bootstrap Class Loader):
這個類加載器負責加載存放在
<JAVA_HOME>\lib目錄幕庐,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機能夠
識別的(按照文件名識別家淤,如rt.jar异剥、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類
庫加載到虛擬機的內(nèi)存中媒鼓。
2. 擴展類加載器(Extension Class Loader):
這個類加載器是在類sun.misc.Launcher$ExtClassLoader中以Java代碼的形式實現(xiàn)的。它負責加載<JAVA_HOME>\lib\ext目錄中错妖,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類庫绿鸣。
3. 應用程序類加載器(Application Class Loader):
由于應用程序類加載器是ClassLoader類中的getSystem?ClassLoader()方法的返回值,所以有些場合中也稱它為“系統(tǒng)類加載器”暂氯。它負責加載用戶類路徑(ClassPath)上所有的類庫潮模,開發(fā)者同樣可以直接在代碼中使用這個類加載器。如果應用程序中沒有自定義過自己的類加載器痴施,一般情況下這個就是程序中默認的類加載器
雙親委派模型的工作過程
如果一個類加載器收到了類加載的請求擎厢,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成辣吃,每一個層次的類加載器都是如此动遭,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時神得,子加載器才會嘗試自己去完成加載厘惦。
雙親委派模型的實現(xiàn)
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 首先,檢查請求的類是否已經(jīng)被加載過了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器拋出ClassNotFoundException
// 說明父類加載器無法完成加載請求
}
if (c == null) {
// 在父類加載器無法加載時
// 再調用本身的findClass方法來進行類加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}