jvm的類加載機制
jvm加載機制對于大多數(shù)人來說都是比較陌生的池户,但是當你作為一個擁有幾年工作經驗的開發(fā)來說动羽,了解內部機制的是極其重要的墨闲,下面先看下代碼佩迟!
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);
}
}
運行結果
SSClass
SuperClass init!
123
類加載過程
類從被加載到虛擬機內存中開始,到卸載出內存為止昏兆,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)爬虱、解析(Resolution)隶债、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段跑筝。其中準備死讹、驗證、解析3個部分統(tǒng)稱為連接(Linking)曲梗。如圖所示赞警。
加載、驗證虏两、準備愧旦、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始定罢,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始笤虫,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)。以下陳述的內容都已HotSpot為基準祖凫。
一琼蚯、 加載
在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流(并沒有指明要從一個Class文件中獲取惠况,可以從其他渠道遭庶,譬如:網絡、動態(tài)生成稠屠、數(shù)據庫等)峦睡;
- 將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據結構;
在內存中生成一個代表這個類的java.lang.Class對象完箩,作為方法區(qū)這個類的各種數(shù)據的訪問入口赐俗; - 加載階段和連接階段(Linking)的部分內容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成弊知,連接階段可能已經開始阻逮,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的內容秩彤,這兩個階段的開始時間仍然保持著固定的先后順序叔扼。
其實通俗來講就是講類的.class文件中二進制數(shù)據讀入到內存中,將其放在運行時數(shù)據區(qū)的方法區(qū)內漫雷,然后在堆區(qū)創(chuàng)建一個
java.lang.Class對象瓜富,用來封裝類在方法區(qū)內的數(shù)據結構。
注:這一塊原先說了在加載過程中會執(zhí)行靜態(tài)代碼塊降盹,后來經沐小晨曦私信我与柑,該地方與后面再初始化過程執(zhí)行靜態(tài)代碼塊說法自相矛盾,后來查了一番,確實如此价捧,多謝提醒丑念。可以確定的說:
靜態(tài)代碼塊是在初始化的時候執(zhí)行的
二结蟋、連接
類加載完成后就進入了類的連接階段脯倚,連接階段主要分為三個過程分別是:驗證,準備和解析嵌屎。在連接階段推正,主要是將已經讀到內存的類的二進制數(shù)據合并到虛擬機的運行時環(huán)境中去。
驗證
驗證是連接階段的第一步宝惰,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求植榕,并且不會危害虛擬機自身的安全。
驗證階段大致會完成4個階段的檢驗動作:
這個階段主要目的是保證Class流的格式是正確的掌测。主要驗證的內容包括:
- 文件格式的驗證
是否以0xCAFEBABE開頭
版本號是否合理
- 元數(shù)據的驗證
是否有父類
是否繼承了final類
非抽象類實現(xiàn)了所有抽象方法
- 字節(jié)碼驗證
運行檢查
棧數(shù)據類型和操作碼數(shù)據參數(shù)吻合
跳轉指令指定到合理的位置
- 符號引用驗證
常量池中描述類是否存在
訪問的方法或字段是否存在且有足夠的權限
準備
這個階段主要是為對象和變量分配內存内贮,并為類設置初始值(方法區(qū)中)
對于static類型變量在這個階段會為其賦值為默認值,比如
public static int v=5,
在這個階段會為其賦值為v=0汞斧,
public static final int v=5,
而對于static final類型的變量夜郁,在準備階段就會被賦值為正確的值
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口粘勒、字段竞端、類方法、接口方法庙睡、方法類型事富、方法句柄和調用點限定符7類符號引用進行。
初始化
在這個階段主要執(zhí)行類的構造方法乘陪。并且為靜態(tài)變量賦值為初始值统台,執(zhí)行靜態(tài)塊代碼。
所有的Java類只有在對類的首次主動使用時才會被初始化啡邑。主動使用的情況有六中贱勃,其他情況都屬于被動使用:
1、 創(chuàng)建類的實例
2谤逼、訪問某個類或接口的靜態(tài)變量贵扰,或者對該靜態(tài)變量賦值
3、調用類的靜態(tài)方法
4流部、反射(Class.fotName)
5戚绕、初始化一個類的子類
6、Java虛擬機啟動時被標明為啟動類的類(面方法所在的類)
注意:
1枝冀、當Java虛擬機初始化一個類時舞丛,要求他的所有父類都已經被初始化耘子,但是這條規(guī)則并不適合接口。在初始化一個類或接口時瓷马,并不會先初始化它所實現(xiàn)的接口拴还。
2、只有當程序訪問的靜態(tài)變量或靜態(tài)方法確實在當前類或當前接口中定義時欧聘,才可以認為是對類或接口的主動使用。如果靜態(tài)方法或變量在parent中定義端盆,從子類進行調用怀骤,則不會初始化子類。
public class Test
{
static
{
i=0;
System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
}
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);
}
}
輸出結果是什么呢蒋伦?當然是1啦~在準備階段我們知道i=0,然后類初始化階段按照順序執(zhí)行焚鹊,首先執(zhí)行static塊中的i=0,接著執(zhí)行static賦值操作i=1,最后在main方法中獲取i的值為1痕届。
<clinit>()
:虛擬機在裝載一個類初始化的時候調用的
<init>()
:虛擬機類實例類化的時候調用的
<clinit>()方法對于類或者接口來說并不是必需的研叫,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作璧针,那么編譯器可以不為這個類生產<clinit>()方法嚷炉。
接口中不能使用靜態(tài)語句塊化撕,但仍然有變量初始化的賦值操作帆竹,因此接口與類一樣都會生成<clinit>()方法屁柏。但接口與類不同的是靡菇,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法众雷。只有當父接口中定義的變量使用時斗幼,父接口才會初始化辙芍。另外瞻想,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法胞枕。
虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確的加鎖杆煞、同步,如果多個線程同時去初始化一個類曲稼,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法索绪,其他線程都需要阻塞等待,直到活動線程執(zhí)行<clinit>()方法完畢贫悄。如果在一個類的<clinit>()方法中有耗時很長的操作瑞驱,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的窄坦。
虛擬機規(guī)范嚴格規(guī)定了有且只有5中情況(jdk1.7)必須對類進行“初始化”(而加載唤反、驗證凳寺、準備自然需要在此之前開始):
- 遇到
new,getstatic,putstatic,invokestatic
這失調字節(jié)碼指令時,如果類沒有進行過初始化彤侍,則需要先觸發(fā)其初始化肠缨。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態(tài)字段(被final修飾盏阶、已在編譯器把結果放入常量池的靜態(tài)字段除外)的時候晒奕,以及調用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候名斟,如果類沒有進行過初始化脑慧,則需要先觸發(fā)其初始化。
- 當初始化一個類的時候砰盐,如果發(fā)現(xiàn)其父類還沒有進行過初始化闷袒,則需要先觸發(fā)其父類的初始化。
- 當虛擬機啟動時岩梳,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)囊骤,虛擬機會先初始化這個主類。
- 當使用jdk1.7動態(tài)語言支持時冀值,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄也物,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發(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;
}
結果為什么呢焦除??作彤?
如果按照下面的正常邏輯:
java中賦值順序:
- 父類的靜態(tài)變量賦值
- 自身的靜態(tài)變量賦值
- 父類成員變量賦值和父類塊賦值
- 父類構造函數(shù)賦值
- 自身成員變量賦值和自身塊賦值
- 自身構造函數(shù)賦值
最終得出的結果是
1,4
但是這肯定是錯的膘魄,正確的打印的結果是
2
3
a=110,b=0
1
4
是不是有點不可思議,那么聽我慢慢分析=呋洹创葡!
首先類的生命周期:
加載->驗證->準備->解析->初始化->使用->卸載
在這些過程中,只有在準備階段和初始化階段會涉及到類和變量的賦值和初始化绢慢;
準備階段
該階段需要做的就是類變量的內存分配和設置默認值
static int b =112;
這里對b進行初始化灿渴,但是初始化值b為0,如果添加了 final的話胰舆,則b=112
初始化階段
初始化階段需要做的是執(zhí)行類構造器骚露,
所以首先執(zhí)行的是:
類的初始化階段需要做是執(zhí)行類構造器(類構造器是編譯器收集所有靜態(tài)語句塊和類變量的賦值語句按語句在源碼中的順序合并生成類構造器,對象的構造方法是<init>()
缚窿,類的構造方法是<clinit>()
棘幸,在代碼中我們可以看到第一個靜態(tài)變量的是:static StaticTest st = new StaticTest();
因此先執(zhí)行第一條靜態(tài)變量的賦值語句即st = new StaticTest (),
然后此時會進行對象的初始化:-->初始化成員變量-->執(zhí)行構造方法因此設置a為110->打印2->執(zhí)行構造方法(打印3,此時a已經賦值為110倦零,但是b只是設置了默認值0误续,并未完成賦值動作)吨悍,等對象的初始化完成后繼續(xù)執(zhí)行之前的類構造器的語句,接下來就不詳細說了蹋嵌,按照語句在源碼中的順序執(zhí)行即可育瓜。
?
這里面還牽涉到一個冷知識,就是在嵌套初始化時有一個特別的邏輯栽烂。特別是內嵌的這個變量恰好是個靜態(tài)成員躏仇,而且是本類的實例。
這會導致一個有趣的現(xiàn)象:“實例初始化竟然出現(xiàn)在靜態(tài)初始化之前”愕鼓。
其實并沒有提前钙态,你要知道java記錄初始化與否的時機。
將上訴案例代碼簡化一下
public class Test {
public static void main(String[] args) {
func();
}
static Test st = new Test();
static void func(){}
}
- 首先在執(zhí)行此段代碼時菇晃,首先由main方法的調用觸發(fā)靜態(tài)初始化。
- 在初始化Test 類的靜態(tài)部分時蚓挤,遇到st這個成員磺送。
- 但湊巧這個變量引用的是本類的實例。
- 那么問題來了灿意,此時靜態(tài)初始化過程還沒完成就要初始化實例部分了估灿。是這樣么?
- 從人的角度是的缤剧。但從java的角度馅袁,一旦開始初始化靜態(tài)部分,無論是否完成荒辕,后續(xù)都不會再重新觸發(fā)靜態(tài)初始化流程了汗销。
- 因此在實例化st變量時,實際上是把實例初始化嵌入到了靜態(tài)初始化流程中抵窒,并且在樓主的問題中弛针,嵌入到了靜態(tài)初始化的起始位置。這就導致了實例初始化完全至于靜態(tài)初始化之前李皇。這也是導致a有值b沒值的原因削茁。
最后再考慮到文本順序,結果就顯而易見了掉房。