1、為什么要了解虛擬機(jī)的類加載機(jī)制?
public class SSClass
{
static
{
System.out.println("SSClass init!");
}
}
public class SuperClass extends SSClass
{
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("SuperClass Construct!");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init!");
}
static int a;
public SubClass()
{
System.out.println("SubClass Construct!");
}
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
2、類的加載過程
把class文件加載到內(nèi)存中,使虛擬機(jī)可識(shí)別成我們寫的java代碼。
定義:虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析3個(gè)階段統(tǒng)稱為連接,這7個(gè)階段的發(fā)生順序如下圖所示:

2.1、加載(Loading)
數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化(靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu))和類在方法區(qū)的初始化。
加載是類加載的一個(gè)階段,主要完成以下三件事:
通過一個(gè)類的全限定名來獲取定義此類的二進(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è)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。該Class類對(duì)象沒有明確規(guī)定是在Java堆中,對(duì)于HotSpot虛擬機(jī)而言,Class對(duì)象比較特殊,它雖然是對(duì)象,但是存放在方法區(qū)里面。
2.2、驗(yàn)證(Verification)
這一階段的目的是為了確保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開頭;
主、次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi);
常量池的常量是否有不被支持的類型;
-
元數(shù)據(jù)驗(yàn)證:驗(yàn)證字節(jié)碼的描述信息,保證其描述的信息系符合Java語言規(guī)范的要求,例如:
這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)該有父類);
這個(gè)類是否繼承了不允許繼承的類(被final修飾的類);
如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法;
-
字節(jié)碼驗(yàn)證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的,例如:
保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上;
保證方法體中的類型轉(zhuǎn)換是有效的;
-
符號(hào)引用驗(yàn)證:可以看做是對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),例如:
符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類;
符號(hào)引用中的類、字段、方法的訪問性(private、protected、public、default)是否可以被當(dāng)前類訪問;
2.3、準(zhǔn)備(Preparation)
在方法區(qū)中類變量分配內(nèi)存并設(shè)置初值。
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。
在準(zhǔn)備階段要注意以下兩點(diǎn)
這時(shí)進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中;
變量的初始值在“通常情況”下是數(shù)據(jù)類型的零值:

但也會(huì)有一些“特殊情況”,若類字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段value就會(huì)被初始化為ConstantValue屬性所指定的值,例如:public static final int value = 123;
2.4、解析(Resolution)
將符號(hào)引用替換為直接引用。
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程,解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。符號(hào)引用和直接引用的關(guān)聯(lián)如下:
-
符號(hào)引用
符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)無歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以不同,但是它們能夠接受的符號(hào)引用必須是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中。
-
直接引用
直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能簡介定位到目標(biāo)的句柄,直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)上翻譯出來的直接引用一般會(huì)不相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
2.5、初始化(Initialization)
執(zhí)行類構(gòu)造器clinit()的過程。
初始化是類加載過程的最后一步,此階段根據(jù)程序員編寫的程序制定的計(jì)劃去初始化類變量和其他資源,或者可以說初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊(static{ }塊)中的語句合并產(chǎn)生的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的語句塊可以賦值,但是不能訪問。
<clinit>()方法與類的構(gòu)造函數(shù)(或者說實(shí)例構(gòu)造器<init>()方法)不同,它不需要顯示地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的<clinit>()方法執(zhí)行之前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。
由于父類的<clinit>()方法先執(zhí)行,所以父類的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。
<clinit>()方法對(duì)于類或者接口來說不是必需的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
接口中不能有靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口也會(huì)生成<clinit>()方法,與普通類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法,只有當(dāng)父接口的變量使用時(shí),才會(huì)初始化,另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法。
虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確的加載、同步。如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其它線程都需要阻塞等待。
虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有5種情況必須立即對(duì)類進(jìn)行初始化(而加載、驗(yàn)證、準(zhǔn)備要在此之前開始):
遇到new、getstatic、putstatic、invokestatic四個(gè)字節(jié)碼指令,對(duì)應(yīng)java場景就是new關(guān)鍵字實(shí)例化對(duì)象、讀取或者設(shè)置一個(gè)靜態(tài)字段(被final修飾、已在編譯期將結(jié)果放入常量池的靜態(tài)變量除外)以及調(diào)用一個(gè)類的靜態(tài)方法。
使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用。
初始化一個(gè)類,但是發(fā)現(xiàn)其父類還沒有初始化,此時(shí)會(huì)觸發(fā)父類初始化。
虛擬機(jī)啟動(dòng)的時(shí)候需要指定一個(gè)要執(zhí)行的主類,進(jìn)行初始化
除了上述5中場景之外,其他所有引用類的方式都不會(huì)觸發(fā)初始化,被稱為被動(dòng)引用,比如:
對(duì)于靜態(tài)字段,只有直接定義了這個(gè)字段的類才會(huì)被初始化,因此通過子類來引用父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化。(開始的例子)
在創(chuàng)建數(shù)組時(shí)并不會(huì)初始化,在編程不注意的時(shí)候可能常常因?yàn)闆]有初始化數(shù)組導(dǎo)致空指針的情況。它僅僅做了創(chuàng)建一個(gè)大小為10的數(shù)組。
final修飾的常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接飲用到定義常量的類。
2.6、類加載器-雙親委派模型
類加載器雖然只用于實(shí)現(xiàn)類的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)遠(yuǎn)不限于類加載階段。類加載主要有以下幾個(gè)作用:
通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流;
審查每一類應(yīng)該有誰加載,它是一種父優(yōu)先的等級(jí)加載機(jī)制;
將Class字節(jié)碼重新解析成JVM統(tǒng)一要求的對(duì)象格式。
對(duì)于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類加載器,都有一個(gè)獨(dú)立的類名稱空間。即比較兩個(gè)類是否相等,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則,即使這兩個(gè)類來源于同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。這里說的相等,包括equals()方法、isAssignableFrom()方法、isInstance()方法的返回結(jié)果,也包括使用instanceof關(guān)鍵字做對(duì)象所屬關(guān)系判定等情況。
從JVM的角度來看,只存在兩種類加載器:
啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)類加載器使用C++語言實(shí)現(xiàn),是虛擬機(jī)自身的一部分;
所有其他的類加載器,這些類加載器都由Java語言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且全部繼承自java.lang.ClassLoader。
從Java開發(fā)人員的角度來看,類加載器的劃分更為細(xì)致,大部分Java程序都會(huì)用到以下3種系統(tǒng)提供的類加載器:
啟動(dòng)類加載器(Bootstrap ClassLoader):這個(gè)類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會(huì)被加載)類庫加載到虛擬機(jī)內(nèi)存中。
擴(kuò)展類加載器(Extension ClassLoader):這個(gè)加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。
應(yīng)用程序類加載器(Application ClassLoader):這個(gè)類加載器由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn),這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,如果應(yīng)用程序中,沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
這些類加載器之間的關(guān)系如下:

這種層次關(guān)系,稱為類加載器的雙親委派模型(Parents Delegation Model),該模型要求除了頂層的啟動(dòng)類加載器之外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。類加載器之間的父子關(guān)系一般不會(huì)以繼承的方式實(shí)現(xiàn),而是使用組合關(guān)系來復(fù)用父加載器的代碼。
雙親委派模型的工作過程是:如果一個(gè)類加載器收到了加載類的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的類加載器中。只有當(dāng)父類加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求時(shí),子加載器才會(huì)嘗試自己去加載。
這種模式的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給啟動(dòng)類加載器加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。相反,若是沒有雙親委派模型,由各個(gè)類加載器自行去加載的話,如果用戶自己編寫了一個(gè)稱為java.lang.Object的類,并放在ClassPath中,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類,應(yīng)用程序?qū)?huì)變得一片混亂。
jvm中并不是所有的類加載都符合雙親委派模型,存在破壞雙親委派的情況~
3、總結(jié)
回答:為什么要了解虛擬機(jī)的類加載機(jī)制?
1、寫出更加可靠、優(yōu)雅的代碼
2、幫助定位、解決問題
3、苦練基本功,多多修煉內(nèi)功總是有好處的