jvm的類加載機(jī)制
jvm加載機(jī)制對于大多數(shù)人來說都是比較陌生的,但是當(dāng)你作為一個擁有幾年工作經(jīng)驗(yàn)的開發(fā)來說,了解內(nèi)部機(jī)制的是極其重要的,下面先看下代碼!
public class SSClass
{
static
{
System.out.println("SSClass");
}
}
public class SuperClass extends SSClass
{
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("init SubClass");
}
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
運(yùn)行結(jié)果
SSClass
SuperClass init!
123
類加載過程
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準(zhǔn)備、驗(yàn)證、解析3個部分統(tǒng)稱為連接(Linking)。如圖所示。

加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運(yùn)行時綁定(也稱為動態(tài)綁定或晚期綁定)。以下陳述的內(nèi)容都已HotSpot為基準(zhǔn)。
一、 加載
在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機(jī)需要完成以下3件事情:

- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流(并沒有指明要從一個Class文件中獲取,可以從其他渠道,譬如:網(wǎng)絡(luò)、動態(tài)生成、數(shù)據(jù)庫等);
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu);
在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口; - 加載階段和連接階段(Linking)的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證動作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進(jìn)行的動作,仍然屬于連接階段的內(nèi)容,這兩個階段的開始時間仍然保持著固定的先后順序。
其實(shí)通俗來講就是講類的.class文件中二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在運(yùn)行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個
java.lang.Class對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。
注:這一塊原先說了在加載過程中會執(zhí)行靜態(tài)代碼塊,后來經(jīng)沐小晨曦私信我,該地方與后面再初始化過程執(zhí)行靜態(tài)代碼塊說法自相矛盾,后來查了一番,確實(shí)如此,多謝提醒??梢源_定的說:
靜態(tài)代碼塊是在初始化的時候執(zhí)行的
二、連接
類加載完成后就進(jìn)入了類的連接階段,連接階段主要分為三個過程分別是:驗(yàn)證,準(zhǔn)備和解析。在連接階段,主要是將已經(jīng)讀到內(nèi)存的類的二進(jìn)制數(shù)據(jù)合并到虛擬機(jī)的運(yùn)行時環(huán)境中去。
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。
驗(yàn)證階段大致會完成4個階段的檢驗(yàn)動作:
這個階段主要目的是保證Class流的格式是正確的。主要驗(yàn)證的內(nèi)容包括:
- 文件格式的驗(yàn)證
是否以0xCAFEBABE開頭
版本號是否合理
- 元數(shù)據(jù)的驗(yàn)證
是否有父類
是否繼承了final類
非抽象類實(shí)現(xiàn)了所有抽象方法
- 字節(jié)碼驗(yàn)證
運(yùn)行檢查
棧數(shù)據(jù)類型和操作碼數(shù)據(jù)參數(shù)吻合
跳轉(zhuǎn)指令指定到合理的位置
- 符號引用驗(yàn)證
常量池中描述類是否存在
訪問的方法或字段是否存在且有足夠的權(quán)限
準(zhǔn)備
這個階段主要是為對象和變量分配內(nèi)存,并為類設(shè)置初始值(方法區(qū)中)
對于static類型變量在這個階段會為其賦值為默認(rèn)值,比如
public static int v=5,
在這個階段會為其賦值為v=0,
public static final int v=5,
而對于static final類型的變量,在準(zhǔn)備階段就會被賦值為正確的值
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號引用進(jìn)行。
初始化
在這個階段主要執(zhí)行類的構(gòu)造方法。并且為靜態(tài)變量賦值為初始值,執(zhí)行靜態(tài)塊代碼。
所有的Java類只有在對類的首次主動使用時才會被初始化。主動使用的情況有六中,其他情況都屬于被動使用:
1、 創(chuàng)建類的實(shí)例
2、訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
3、調(diào)用類的靜態(tài)方法
4、反射(Class.fotName)
5、初始化一個類的子類
6、Java虛擬機(jī)啟動時被標(biāo)明為啟動類的類(面方法所在的類)
注意:
1、當(dāng)Java虛擬機(jī)初始化一個類時,要求他的所有父類都已經(jīng)被初始化,但是這條規(guī)則并不適合接口。在初始化一個類或接口時,并不會先初始化它所實(shí)現(xiàn)的接口。
2、只有當(dāng)程序訪問的靜態(tài)變量或靜態(tài)方法確實(shí)在當(dāng)前類或當(dāng)前接口中定義時,才可以認(rèn)為是對類或接口的主動使用。如果靜態(tài)方法或變量在parent中定義,從子類進(jìn)行調(diào)用,則不會初始化子類。
public class Test
{
static
{
i=0;
System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應(yīng)用)
}
static int i=1;
}
那么去掉報錯的那句,改成下面:
public class Test
{
static
{
i=0;
// System.out.println(i);
}
static int i=1;
public static void main(String args[])
{
System.out.println(i);
}
}
輸出結(jié)果是什么呢?當(dāng)然是1啦~在準(zhǔn)備階段我們知道i=0,然后類初始化階段按照順序執(zhí)行,首先執(zhí)行static塊中的i=0,接著執(zhí)行static賦值操作i=1,最后在main方法中獲取i的值為1。
<clinit>():虛擬機(jī)在裝載一個類初始化的時候調(diào)用的
<init>():虛擬機(jī)類實(shí)例類化的時候調(diào)用的
<clinit>()方法對于類或者接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生產(chǎn)<clinit>()方法。
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有當(dāng)父接口中定義的變量使用時,父接口才會初始化。另外,接口的實(shí)現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法。
虛擬機(jī)會保證一個類的<clinit>()方法在多線程環(huán)境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個線程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是隱藏的。
虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有5中情況(jdk1.7)必須對類進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開始):
- 遇到
new,getstatic,putstatic,invokestatic這失調(diào)字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實(shí)例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機(jī)會先初始化這個主類。
- 當(dāng)使用jdk1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行初始化,則需要先出觸發(fā)其初始化。
案例分析
package jvm.classload;
public class StaticTest
{
public static void main(String[] args)
{
staticFunction();
}
static StaticTest st = new StaticTest();
static
{
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest()
{
System.out.println("3");
System.out.println("a="+a+",b="+b);
}
public static void staticFunction(){
System.out.println("4");
}
int a=110;
static int b =112;
}
結(jié)果為什么呢???
如果按照下面的正常邏輯:
java中賦值順序:
- 父類的靜態(tài)變量賦值
- 自身的靜態(tài)變量賦值
- 父類成員變量賦值和父類塊賦值
- 父類構(gòu)造函數(shù)賦值
- 自身成員變量賦值和自身塊賦值
- 自身構(gòu)造函數(shù)賦值
最終得出的結(jié)果是
1,4
但是這肯定是錯的,正確的打印的結(jié)果是
2
3
a=110,b=0
1
4
是不是有點(diǎn)不可思議,那么聽我慢慢分析!!
首先類的生命周期:
加載->驗(yàn)證->準(zhǔn)備->解析->初始化->使用->卸載
在這些過程中,只有在準(zhǔn)備階段和初始化階段會涉及到類和變量的賦值和初始化;
準(zhǔn)備階段
該階段需要做的就是類變量的內(nèi)存分配和設(shè)置默認(rèn)值
static int b =112;
這里對b進(jìn)行初始化,但是初始化值b為0,如果添加了 final的話,則b=112
初始化階段
初始化階段需要做的是執(zhí)行類構(gòu)造器,
所以首先執(zhí)行的是:
類的初始化階段需要做是執(zhí)行類構(gòu)造器(類構(gòu)造器是編譯器收集所有靜態(tài)語句塊和類變量的賦值語句按語句在源碼中的順序合并生成類構(gòu)造器,對象的構(gòu)造方法是<init>(),類的構(gòu)造方法是<clinit>(),在代碼中我們可以看到第一個靜態(tài)變量的是:static StaticTest st = new StaticTest();因此先執(zhí)行第一條靜態(tài)變量的賦值語句即st = new StaticTest (),
然后此時會進(jìn)行對象的初始化:-->初始化成員變量-->執(zhí)行構(gòu)造方法因此設(shè)置a為110->打印2->執(zhí)行構(gòu)造方法(打印3,此時a已經(jīng)賦值為110,但是b只是設(shè)置了默認(rèn)值0,并未完成賦值動作),等對象的初始化完成后繼續(xù)執(zhí)行之前的類構(gòu)造器的語句,接下來就不詳細(xì)說了,按照語句在源碼中的順序執(zhí)行即可。
?
這里面還牽涉到一個冷知識,就是在嵌套初始化時有一個特別的邏輯。特別是內(nèi)嵌的這個變量恰好是個靜態(tài)成員,而且是本類的實(shí)例。
這會導(dǎo)致一個有趣的現(xiàn)象:“實(shí)例初始化竟然出現(xiàn)在靜態(tài)初始化之前”。
其實(shí)并沒有提前,你要知道java記錄初始化與否的時機(jī)。
將上訴案例代碼簡化一下
public class Test {
public static void main(String[] args) {
func();
}
static Test st = new Test();
static void func(){}
}
- 首先在執(zhí)行此段代碼時,首先由main方法的調(diào)用觸發(fā)靜態(tài)初始化。
- 在初始化Test 類的靜態(tài)部分時,遇到st這個成員。
- 但湊巧這個變量引用的是本類的實(shí)例。
- 那么問題來了,此時靜態(tài)初始化過程還沒完成就要初始化實(shí)例部分了。是這樣么?
- 從人的角度是的。但從java的角度,一旦開始初始化靜態(tài)部分,無論是否完成,后續(xù)都不會再重新觸發(fā)靜態(tài)初始化流程了。
- 因此在實(shí)例化st變量時,實(shí)際上是把實(shí)例初始化嵌入到了靜態(tài)初始化流程中,并且在樓主的問題中,嵌入到了靜態(tài)初始化的起始位置。這就導(dǎo)致了實(shí)例初始化完全至于靜態(tài)初始化之前。這也是導(dǎo)致a有值b沒值的原因。
最后再考慮到文本順序,結(jié)果就顯而易見了。