轉(zhuǎn)自 http://blog.csdn.net/wen7280/article/details/53856790
圖覽全局----Class文件裝載經(jīng)歷的各個階段:
在Java應用程序開發(fā)中挠轴,只有被java虛擬機裝載的Class類型才能在程序中使用践瓷。只要生成的字節(jié)碼符合java虛擬機的指令集和文件格式蓖租,就可以在JVM上運行劫瞳,這為java的跨平臺性提供條件。
字節(jié)碼文件的裝載過程:加載 饶碘、 ?連接(包括三個步驟:驗證 ?準備 ? 解析) ?、初始化,如圖所示
-------------------------------------------------------------------------------------------------
類裝載的條件:
Java虛擬機不會無條件的裝載Class類型表悬。
Java虛擬機規(guī)定:一個類或者接口在初次使用時,必須進行初始化丧靡。
這里的使用指的是主動使用蟆沫,主動使用有以下幾種情況:
當創(chuàng)建一個類的實例時,比如使用new關鍵字温治,或者通過反射饭庞、克隆、反序列化方式熬荆。
當調(diào)用類的靜態(tài)方法時舟山,即當使用了字節(jié)碼invokestatic指令
當使用類或者接口的靜態(tài)字段時(final常量除外,此種情況只會加載類而不會進行初始化)卤恳,即使用getstatic或者putstatic指令(可以使用jclasslib軟件查看生成的字節(jié)碼文件)
當使用java.lang.reflect包中的方法反射類的方法時
當初始化子類時累盗,必須先初始化父類
作為啟動虛擬機、含有main方法的那個類
除了以上情況屬于主動使用外突琳,其他情況均屬于被動使用若债,被動使用不會引起類的初始化,只是加載了類卻沒有初始化拆融。
例1:主動使用(這是三個class文件蠢琳,而不是一個,此處為方便寫在一起冠息。多說一點:因為一個Class文件只能有一個public類和文件名一樣挪凑,其余類修飾符只能是非pubic)
[java]view plaincopy
publicclassParent{
static{
System.out.println("Parent?init");
}
}
publicclassChild{
static{
System.out.println("Child?init");
}
}
publicclassInitMain{
publicstaticvoidmain(String[]?args){
Child?c?=newChild();
}
}
以上聲明了3個類:Parent Child InitMain,Child類為Parent類的子類逛艰。若Parent被初始化躏碳,將會執(zhí)行static塊,會打印"Parent init"散怖,若Child被初始化菇绵,則會打印"Child init"。(類的初始化先于加載镇眷,故執(zhí)行靜態(tài)代碼塊后咬最,就表明類已經(jīng)加載了)
[java]view plaincopy
執(zhí)行InitMain,結果為:
Parent?init
Child?init
由此可知欠动,系統(tǒng)首先裝載Parent類永乌,接著裝載Child類惑申。
符合主動裝載中的兩個條件:使用new關鍵字創(chuàng)建類的實例會裝載相關的類,以及在初始化子類時翅雏,必須先初始化父類圈驼。
例2 :被動裝載
[java]view plaincopy
publicclassParent{
static{
System.out.println("Parent?init?");
}
publicstaticintv?=100;//靜態(tài)字段
}
publicclassChildextendsParent{
static{
System.out.println("Child?init");
}
}
publicclassUserParent{
publicstaticvoidmain(String[]?args){
System.out.println(Child.v);
}
}
Parent中有靜態(tài)變量v,并且在UserParent中望几,使用其子類Child去調(diào)用父類中的變量绩脆。
[java]view plaincopy
運行代碼:
Parent?init
100
雖然在UserParent中,直接訪問了子類對象橄抹,但是Child子類并未初始化靴迫,僅僅加載了Child類,只有Parent類進行初始化楼誓。所以玉锌,在引用一個字段時,只有直接定義該字段的類慌随,才會被初始化芬沉。
注意:雖然Child類沒有被初始化,但是阁猜,此時Child類已經(jīng)被系統(tǒng)加載丸逸,只是沒有進入初始化階段。
可以使用-XX:+ThraceClassLoading 參數(shù)運行這段代碼剃袍,查看日志黄刚,便可以看到Child類確實被加載了,只是初始化沒有進行
例3 :引用final常量
[java]view plaincopy
publicclassFinalFieldClass{
publicstaticfinalString?constString?="CONST";
static{
System.out.println("FinalFieldClass?init");
}
}
publicclassUseFinalField{
publicstaticvoidmain(String[]?args){
System.out.println(FinalFieldClass.constString);
}
}
運行代碼:CONST
FinalFieldClass類沒有因為其常量字段constString被引用而進行初始化民效,這是因為在Class文件生成時憔维,final常量由于其不變性,做了適當?shù)膬?yōu)化畏邢。驗證完字節(jié)碼文件無誤后业扒,在準備階段就會為常量初始化為指定的值。
分析UseFinalField類生成的Class文件舒萎,可以看到main函數(shù)的字節(jié)碼為:
在字節(jié)碼偏移3的位置程储,通過Idc將常量池第22項入棧,在此Class文件中常量池第22項為:
#22 = String ? ? ? ?#23 ? ? //CONST
#23 = UTF8 ? ? ? ? CONST
由此可以看出臂寝,編譯后的UseFinalField.class中章鲤,并沒有引用FinalFieldClass類,而是將FinalFieldClass類中final常量字段直接存放在自己的常量池中咆贬,所以败徊,F(xiàn)inalFiledClass類自然不會被加載。(javac在編譯時掏缎,將常量直接植入目標類皱蹦,不再使用被引用類)通過捕獲類加載日志(部分日志)可以看出:(并沒有加載FinalFiledClass類日志)
注意:并不是在代碼中出現(xiàn)的類煤杀,就一定會被加載或者初始化,如果不符合主動使用的條件根欧,類就不會被加載或者進一步初始化怜珍。
詳解類裝載的整個過程
1)加載類:處于類裝載的第一個階段。
加載類時凤粗,JVM必須完成:
通過類的全名,獲取類的二進制數(shù)據(jù)流
解析類的二進制數(shù)據(jù)流為方法區(qū)內(nèi)的數(shù)據(jù)結構今豆,也就是將類文件放入方法區(qū)中
創(chuàng)建java.lang.Class類的實例嫌拣,表示該類型
2)連接
驗證字節(jié)碼文件:當類被加載到系統(tǒng)后,就開始連接操作呆躲,驗證是連接的第一步异逐。
主要目的是保證加載的字節(jié)碼是符合規(guī)范的。
驗證的步驟如圖:
準備階段
當一個類驗證通過后插掂,虛擬機就會進入準備階段灰瞻。準備階段是正式為類變量(static修飾的變量)分配內(nèi)存并設置類變量初始值,這些內(nèi)存都將在方法區(qū)進行分配辅甥。這個時候進行內(nèi)存分配的僅是類變量酝润,不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在堆上璃弄。為類變量設置初始值是設為其數(shù)據(jù)類型的“零值”要销。
比如 public static int num = 12; 這個時候就會為num變量賦值為0
java虛擬機為各種類型變量默認的初始值如表:
類型默認初始值
int0
long0L
short(short)0
char\u0000
booleanfalse
referencenull
float0f
double0f
注意:java并不支持boolean類型,對于boolean類型夏块,內(nèi)部實現(xiàn)是Int疏咐,由于int的默認值是0,故對應的脐供,boolean的默認值是false
如果類中屬于常量的字段浑塞,那么常量字段也會在準備階段被附上正確的值,這個賦值屬于java虛擬機的行為政己,屬于變量的初始化酌壕。在準備階段,不會有任何java代碼被執(zhí)行匹颤。
解析類
在準備階段完成后仅孩,就進入了解析階段。
解析階段的任務就是將類印蓖、接口辽慕、字段和方法的符號引用轉(zhuǎn)為直接引用。
符號引用就是一些字面量的引用赦肃。比較容易理解的就是在Class類文件中溅蛉,通過常量池進行大量的符號引用公浪。
具體可以使用JclassLib軟件查看Class文件的結構:::
下面通過一個簡單函數(shù)的調(diào)用來講解下符號引用是如何工作的。船侧。欠气。
例如:System.out.println();
生成的字節(jié)碼指令:invokevirtual #24
這里使用了常量池第24項,查看并分析該常量池镜撩,可以查看到如圖的結構:
常量池第24項被invokevirtual使用预柒,順著CONSTANT_Methodref #24的引用關系繼續(xù)在常量池中查找,發(fā)現(xiàn)所有對于Class以及NameAndType類型的引用都是基于字符串的袁梗,因此宜鸯,可以認為Invokevirtual的函數(shù)調(diào)用通過字面量的引用描述已經(jīng)表達清楚了,這就是符合引用遮怜。
但是只有符合引用是不夠的淋袖,當println()方法被調(diào)用時,系統(tǒng)需要明確知道方法的位置锯梁。java虛擬機會為每個類準備一張方法表即碗,將其所有的方法都列在表中,當需要調(diào)用一個類的方法時陌凳,只要知道這個方法在表中的偏移量就可以了剥懒。通過解析操作,符合引用就可以轉(zhuǎn)變?yōu)槟繕朔椒ㄔ陬愔蟹椒ū淼奈恢梅胨欤瑥亩狗椒ū怀晒φ{(diào)用蕊肥。
所以,解析的目的就是將符合引用轉(zhuǎn)變?yōu)橹苯右酶蚣。褪堑玫筋惢蛘咦侄伪谌础⒎椒ㄔ趦?nèi)存中的指針或者偏移量。如果直接引用存在裸准,那么系統(tǒng)中肯定存在類展东、方法或者字段,但只存在符合引用炒俱,不能確定系統(tǒng)中一定存在該對象盐肃。
3)類初始化
如果前面的步驟沒有出現(xiàn)問題,那么表示類可以順利裝載到系統(tǒng)中权悟。此時砸王,才會開始執(zhí)行java字節(jié)碼。
初始化階段的重要工作是執(zhí)行類的初始化方法()峦阁。其特點:
()方法是由編譯器自動生成的馏段,它是由類靜態(tài)成員的賦值語句以及static語句塊合并產(chǎn)生的婶博。編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的慷暂,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的類變量,定義在其之后的類變量瘪菌,只能被賦值,不能被訪問嘹朗。比如:
static{
num = 5;//這是合法的
}
static int num = 12;
static{
System.out.println(num);//這樣是不合法的
}
static int num = 12;
例如:
[java]view plaincopy
publicclassSimpleStatic{
publicstaticintid?=1;
publicstaticintnumber;
static{
number?=4;
}
}
java編譯器為這段代碼生成如下的:
0 iconst_1
1 putstatic #2
4 iconst_4
5 putstatic #3
8 return
函數(shù)中师妙,整合了SimpleStatic類中的static賦值語句以及static語句塊
改段JVM指令代碼表示:先后對id和number兩個成員變量進行賦值
()方法與類的構造器函數(shù)()方法不同,它不需要顯示的調(diào)用父類的()方法屹培,虛擬機會保證在子類的()方法執(zhí)行之前默穴,父類的()方法已經(jīng)執(zhí)行完畢。故父類的靜態(tài)語句塊會先于子類的靜態(tài)語句塊執(zhí)行褪秀。
[java]view plaincopy
publicclassChildStaticextendsSimpleStatic
{
static{
number?=2;
}
publicstaticvoidmain(String[]?args){
System.out.println(number);
}
}
[java]view plaincopy
運行代碼:
2
表明父類的總是在子類之前被調(diào)用壁顶。
注意:java編譯器并不是為所有的類都產(chǎn)生初始化函數(shù),如果一個類既沒有類變量賦值語句溜歪,也沒有static語句塊,那么生成的函數(shù)就應該為空许蓖,因此蝴猪,編譯器就不會為該類插入函數(shù)
例如:
public class StaticFinalClass{
public static final int i=1;
public static final int j=2;
}
由于StaticFinalClass只有final常量,而final常量在準備階段被賦值膊爪,而不在初始化階段處理自阱,因此對于StaticFinalClass類來說,就無事可做米酬,因此沛豌,在產(chǎn)生的class文件中沒有該函數(shù)存在。
虛擬機保證一個類的()方法在多線程環(huán)境中被正確的加鎖和同步赃额,如果多個線程同時去初始化一個類加派,只有一個線程去執(zhí)行這個類的()方法,其他線程都會被阻塞跳芳,直到指定線程執(zhí)行完()方法芍锦。
--------------------------------------------------------------------------------------------------------------------------------------------------
趁著意猶未盡,來看看對象初始化流程:包括成員變量和構造器調(diào)用的先后順序飞盆,子類構造器和父類之間的先后順序等等娄琉。通過字節(jié)碼文件指令直接的展示這個過程:
編輯幾個類,包括一個子類一個父類吓歇,其中子類和父類中都包含了成員變量孽水、非靜態(tài)代碼塊、構造器函數(shù)以及前面講到的靜態(tài)代碼塊和靜態(tài)變量:
[java]view plaincopy
packagecom.classextends;
publicclassFuZiDemo?{
publicstaticvoidmain(String[]?args)?{
newZiClass();//測試類城看,創(chuàng)建子類對象
}
}
[java]view plaincopy
classFuClass?{
intfuOwer?=120;//成員變量一
static{
System.out.println("Fu?clinit()");//靜態(tài)代碼塊
}
staticintnum?=22;//靜態(tài)變量
{//非靜態(tài)代碼塊
fuName?="tempValue";
System.out.println(fuOwer);
intc?=23;
}
String?fuName?="dali";//成員變量二
FuClass(){//父類構造函數(shù)
System.out.println("Fu?init()");
fuOwer?=100;
}
}
[java]view plaincopy
classZiClassextendsFuClass?{
intziOwer?=82;//成員變量一
static{//靜態(tài)代碼塊
System.out.println("Zi?clinit()");
}
staticintnum?=2;//靜態(tài)變量
{//非靜態(tài)代碼塊
ziName?="tempValue";
System.out.println(ziOwer);
intc?=23;//局部變量
}
String?ziName?="urocle";//成員變量二
ZiClass(){//子類構造函數(shù)
ziOwer?=23;
System.out.println("Zi?init()");
}
}
分析:
一女气、類的加載和初始化
首先FuziDemo這個測試類要加載,然后執(zhí)行main指令時會new 子類對象析命,故要去加載子類的字節(jié)碼文件主卫,但是會發(fā)現(xiàn)子類有一個直接繼承類FuClass逃默,于是就會先去加載FuClass的字節(jié)碼文件,接著會初始化父類簇搅,執(zhí)行FuClass類的方法:執(zhí)行輸出語句以及為靜態(tài)成員賦值完域,其字節(jié)碼指令為:
0 getstatic #13
3 ldc#19
5 invokevirtual #21
8 bipush22
10 putstatic#27
13 return
完成父類的初始化工作之后,緊接著加載子類的字節(jié)碼文件并且執(zhí)行其()方法瘩将。其字節(jié)碼指令類似于父類的:
0 getstatic #13
3 ldc#19
5 invokevirtual #21//調(diào)用println()方法輸出 #19也就是 Zi clinit()
8iconst_2
9 putstatic#27//為靜態(tài)變量賦值
12 return
二吟税、子類和父類成員變量初始化,以及構造函數(shù)執(zhí)行順序
測試類main函數(shù)的字節(jié)碼指令:
0 new #16
3 invokespecial #18> ? ? ? ? //調(diào)用子類的初始化函數(shù)
6 return
下面看看子類ZiClass的()函數(shù)的字節(jié)碼指令:
0 aload_0
1 invokespecial #32>//首先會去調(diào)用父類的()函數(shù)
4 aload_0
5 bipush82
7putfield#34//為成員變量 ziOwer賦值為82
10 aload_0
11 ldc #36
13putfield#38//執(zhí)行非靜態(tài)代碼塊姿现,臨時為成員變量ziName賦值
16 getstatic #13//調(diào)用System.out輸出函數(shù)
19 aload_0
20 getfield #34//獲取成員變量 ziOwer的值
23 invokevirtual #40//打印輸出
26 bipush 23
28 istore_1
29 aload_0
30 ldc#43
32putfield#38//為成員變量ziName賦值為urocle
35 aload_0
36bipush 23//取出 23 ,意味著實例初始化過程中先初始化成員變量及執(zhí)行非靜態(tài)代碼塊肠仪,最后執(zhí)行構造
38putfield#34//為成員變量ziOwer賦值為23
41 getstatic #13
44 ldc #45
46 invokevirtual #21
49 return
同樣FuClass類的實例初始化函數(shù)()如下,此處不再解釋:
0 aload_0
1 invokespecial #32>
4 aload_0
5 bipush 120
7 putfield #34
10 aload_0
11 ldc #36
13 putfield #38
16 getstatic #13
19 aload_0
20 getfield #34
23 invokevirtual #40
26 bipush 23
28 istore_1
29 aload_0
30 ldc #43
32 putfield #38
35 getstatic #13
38 ldc #45
40 invokevirtual #21
43 aload_0
44 bipush 100
46 putfield #34
49 return
三 ?給出程序執(zhí)行的結果
Fu clinit()
Zi clinit() ? ? ? ?//靜態(tài)代碼塊輸出
120 ? ? ? ? ? ? ? ? //非靜態(tài)代碼塊輸出
Fu init() ? ? ? ? //構造函數(shù)輸出
82
Zi init()
總結:
(1)父類加載初始化先于子類备典,父類的優(yōu)先于子類的函數(shù)執(zhí)行
(2)如果創(chuàng)建一個子類對象异旧,父類構造函數(shù)調(diào)用先于子類構造器函數(shù)調(diào)用。在執(zhí)行構造器函數(shù)首先會初始化類中成員變量或者執(zhí)行非靜態(tài)代碼塊(這二者執(zhí)行的先后順序依賴于在源文件中出現(xiàn)的順序)提佣,然后再調(diào)用構造函數(shù)吮蛹。