Class文件
我們寫(xiě)的Java代碼,經(jīng)過(guò)編譯器編譯之后,就成為了.class文件,從本地機(jī)器碼變成了字節(jié)碼。Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在Class文件之中,中間沒(méi)有添加任何分隔符,這使得整個(gè)Class文件中存儲(chǔ)的內(nèi)容幾乎全部是程序運(yùn)行的必要數(shù)據(jù),沒(méi)有空隙存在。Class文件中只有2種數(shù)據(jù)結(jié)構(gòu):無(wú)符號(hào)數(shù)和表。
每個(gè)Class文件的頭4個(gè)字節(jié)稱(chēng)為魔數(shù)(Magic Number),值為0xCAFEBABE。緊接著4個(gè)字節(jié)是Class文件的版本號(hào)。再往后,就是類(lèi)的具體信息了,比如常量池、類(lèi)索引、父類(lèi)索引、接口索引、字段、方法等信息了。
所謂類(lèi)的加載,就是把Class文件讀到內(nèi)存中。
類(lèi)的生命周期
類(lèi)從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱(chēng)為連接(Linking)。
加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類(lèi)的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定)。注意,是按部就班地“開(kāi)始”,而不是按部就班地“進(jìn)行”或“完成”,強(qiáng)調(diào)這點(diǎn)是因?yàn)檫@些階段通常都是互相交叉地混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用、激活另外一個(gè)階段。
加載
在加載階段,虛擬機(jī)做3件事:
- 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪(fǎng)問(wèn)入口。
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
驗(yàn)證階段大致上會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。比如是否以魔數(shù)0xCAFEBABE開(kāi)頭,主、次版本號(hào)是否能被當(dāng)前虛擬機(jī)處理,常量類(lèi)型,指向常量的索引是否符合要求等。這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過(guò)了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面的3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。
- 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求。比如繼承關(guān)系。
- 字節(jié)碼驗(yàn)證:對(duì)類(lèi)的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
- 符號(hào)引用驗(yàn)證:對(duì)類(lèi)自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),確保解析動(dòng)作能正常執(zhí)行。它發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。
驗(yàn)證階段是非常重要的,但不是必須的。它對(duì)程序運(yùn)行期沒(méi)有影響,如果所引用的類(lèi)經(jīng)過(guò)反復(fù)驗(yàn)證,那么可以考慮采用-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類(lèi)驗(yàn)證措施,以縮短虛擬機(jī)類(lèi)加載的時(shí)間。
準(zhǔn)備
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段中有兩個(gè)容易產(chǎn)生混淆的概念需要強(qiáng)調(diào)一下,首先,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。其次,這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類(lèi)型的零值。
假設(shè)一個(gè)類(lèi)變量的定義為:public static int value = 123;
那變量value在準(zhǔn)備階段過(guò)后的初始值為0而不是123,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何Java
方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類(lèi)構(gòu)造器<clinit>()方
法之中,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。
當(dāng)然也有特殊情況:如果類(lèi)字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值。
假設(shè)上面類(lèi)變量value的定義變?yōu)椋簆ublic static final int value = 123;
編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符7類(lèi)符號(hào)引用進(jìn)行。
- 符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可
以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的
內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各
不相同,但是它們能接受的符號(hào)引用必須都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義
在Java虛擬機(jī)規(guī)范的Class文件格式中。 - 直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是
一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引
用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目
標(biāo)必定已經(jīng)在內(nèi)存中存在。
初始化
這一步開(kāi)始執(zhí)行類(lèi)中定義的Java程序代碼(或者說(shuō)是字節(jié)碼)。虛擬機(jī)會(huì)保證一個(gè)類(lèi)的初始化方法在多線(xiàn)程環(huán)境中被正確地加鎖、同步,如果多個(gè)線(xiàn)程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有一個(gè)線(xiàn)程去執(zhí)行這個(gè)類(lèi)的初始化方法,其他線(xiàn)程都需要阻塞等待,直到活動(dòng)線(xiàn)程執(zhí)行完畢。
JVM初始化步驟
- 假如這個(gè)類(lèi)還沒(méi)有被加載和連接,則程序先加載并連接該類(lèi)
- 假如該類(lèi)的直接父類(lèi)還沒(méi)有被初始化,則先初始化其直接父類(lèi)
- 假如類(lèi)中有初始化語(yǔ)句,則系統(tǒng)依次執(zhí)行這些初始化語(yǔ)句
類(lèi)初始化時(shí)機(jī)
只有當(dāng)主動(dòng)使用一個(gè)類(lèi)的時(shí)候才會(huì)觸發(fā)這個(gè)類(lèi)的初始化,類(lèi)的主動(dòng)使用包括以下六種:
- 創(chuàng)建類(lèi)的實(shí)例,也就是new的方式
- 訪(fǎng)問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
- 調(diào)用類(lèi)的靜態(tài)方法
- 反射,比如Class.forName(“com.mysql.jdbc.Driver”)
- 初始化某個(gè)類(lèi)的子類(lèi),則其父類(lèi)也會(huì)被初始化
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi)(Java Test),直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類(lèi)
類(lèi)加載器
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類(lèi)加載階段中的“通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取描述此類(lèi)的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓?xiě)?yīng)用程序自己決定如何去獲取所需要的類(lèi)。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱(chēng)為“類(lèi)加載器”。
雙親委派模型
從Java虛擬機(jī)的角度來(lái)講,只存在兩種不同的類(lèi)加載器:一種是啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),這個(gè)類(lèi)加載器使用C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分;另一種就是所有其他的類(lèi)加載器,這些類(lèi)加載器都由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且全都繼承自抽象類(lèi)java.lang.ClassLoader。
從Java開(kāi)發(fā)人員的角度來(lái)看,類(lèi)加載器可以劃分為以下3種:
- 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):負(fù)責(zé)加載存放在JAVA_HOME\lib目錄中的,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機(jī)識(shí)別的類(lèi)庫(kù)(如rt.jar,所有的java.開(kāi)頭的類(lèi)均被Bootstrap ClassLoader加載)。啟動(dòng)類(lèi)加載器是無(wú)法被Java程序直接引用的。
- 擴(kuò)展類(lèi)加載器(Extension ClassLoader):這個(gè)加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。
- 應(yīng)用程序類(lèi)加載器(Application ClassLoader):該類(lèi)加載器由sun.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn),它負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)所指定的類(lèi),開(kāi)發(fā)者可以直接使用該類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。
我們的應(yīng)用程序都是由這3種類(lèi)加載器互相配合進(jìn)行加載的,如果有必要,還可以加入
自己定義的類(lèi)加載器。
雙親委派模型要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)當(dāng)有自己的父類(lèi)加載器。這里類(lèi)加載器之間的父子關(guān)系一般不會(huì)以繼承的關(guān)系來(lái)實(shí)現(xiàn),而是都使用組合關(guān)系來(lái)復(fù)用父加載器的代碼。它不是強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦的一種類(lèi)加載器實(shí)現(xiàn)方式。
雙親委派模型的工作過(guò)程:如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己去加載。
ClassLoader源碼分析:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先判斷該類(lèi)型是否已經(jīng)被加載
Class c = findLoadedClass(name);
if (c == null) {
//如果沒(méi)有被加載,就委托給父類(lèi)加載或者委派給啟動(dòng)類(lèi)加載器加載
try {
if (parent != null) {
//如果存在父類(lèi)加載器,就委派給父類(lèi)加載器加載
c = parent.loadClass(name, false);
} else {
//如果不存在父類(lèi)加載器,就檢查是否是由啟動(dòng)類(lèi)加載器加載的類(lèi),通過(guò)調(diào)用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父類(lèi)加載器和啟動(dòng)類(lèi)加載器都不能完成加載任務(wù),才調(diào)用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通過(guò)分析源碼,我們知道,雙親委派模型可以保證每個(gè)類(lèi)都只會(huì)被加載一次(類(lèi)似緩存機(jī)制)。
參考
- 《深入理解Java虛擬機(jī) 第二版》
- 純潔的微笑