原文地址[http://blog.csdn.net/ns_code/article/details/17881581]
類加載的過程
類從被加載到虛擬機內(nèi)存中開始玻淑,到卸載出內(nèi)存為止,它的整個生命周期包括:加載→驗證→準(zhǔn)備→解析→初始化→使用→卸載
其中類加載的過程包括了加載呀伙、驗證补履、準(zhǔn)備、解析剿另、初始化
五個階段箫锤。在這五個階段中帅腌,加載、驗證麻汰、準(zhǔn)備和初始化這四個階段發(fā)生的順序是確定的,而解析階段則不一定戚篙,它在某些情況下可以在初始化階段后開始五鲫,這是為了支持Java語言的運行時綁定(也叫動態(tài)綁定)。另外注意這里的幾個階段是按順序開始岔擂,而不是按順序進行或完成位喂,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執(zhí)行的過程中調(diào)用或激活另一個階段乱灵。
這里簡要說明下Java中的額綁定:綁定是指把一個方法的調(diào)用與方法所在的類(方法主體)關(guān)聯(lián)起來塑崖,對Java來說,綁定分為動態(tài)綁定和靜態(tài)綁定:
- 靜態(tài)綁定:即前期綁定痛倚。在程序執(zhí)行前方法已被綁定规婆,此時由編譯器或其他連接程序?qū)崿F(xiàn)。針對Java蝉稳,簡單的可以理解為程序編譯器的綁定抒蚜。Java當(dāng)中只有final,static耘戚,private和構(gòu)造方法是前期綁定的嗡髓。
- 動態(tài)綁定:即晚起綁定,也叫運行時綁定收津,在運行時根據(jù)具體對象的類型進行綁定饿这。在Java中,幾乎所有的方法都是后期綁定的撞秋。
下面來詳細(xì)介紹類加載過程中每個階段所做的工作:
加載
加載是類加載過程的第一個階段长捧,在加載階段,虛擬機需要完成下面三件事情:
- 通過一個類的全限定名來獲取其定義的二進制字節(jié)流部服。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運行時數(shù)據(jù)結(jié)構(gòu)唆姐。
- 在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口廓八。
注意奉芦,這里第1條中的二進制字節(jié)流并不是單純的從Class文件中獲取,比如它還可以從Jar包中獲取剧蹂、從網(wǎng)絡(luò)中獲壬Α(最典型的應(yīng)用是Applet),由其他文件生成(JSP應(yīng)用)等宠叼。
相對于類加載的其他階段而言先巴,加載階段(準(zhǔn)確的說是加載階段獲取類的二進制字節(jié)流的動作)是可控性最強的階段其爵,因為開發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載伸蚯。
加載階段完成后摩渺,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中。而且在Java堆哄也創(chuàng)建了一個java.lang.class類的對象剂邮,這樣便可以通過該對象訪問方法區(qū)中的這些數(shù)據(jù)摇幻。
說到了類加載,就不得不提到類加載器挥萌,下面就具體介紹下類加載器
加載器
類加載器雖然只用于實現(xiàn)類的加載動作绰姻,但它在Java程序中起到的作用卻遠(yuǎn)遠(yuǎn)不限于類的加載階段。對于任意一個類引瀑,都需要由它的類加載器和這個類本身一同來確定這個類在Java虛擬機中的唯一性狂芋,也就是說,即使這兩個類來源同一個Class文件憨栽,只要加載他們的類加載器不同帜矾,那這兩個類就必定不相等。這里的“相等”包括類代表累的Class對象的equals(), isAssignableFrom(), isInstance()等方法的返回結(jié)果徒像,也包括了使用instanceof關(guān)鍵字對對象所屬關(guān)系的判定結(jié)果黍特。
在JVM的角度來說,只存在兩種不同的類加載器:
- 啟動類加載器:它使用C++實現(xiàn)(這里僅限于Hotspot锯蛀,也就是JDK1.5以后的默認(rèn)虛擬機灭衷,有很多其他的虛擬機是用Java語言實現(xiàn)的),是虛擬機的一部分旁涤。
- 所有其他的類加載器這些類加載器都是有Java語言實現(xiàn)翔曲,獨立于虛擬機之外,并且全部繼承自抽象類java.lang.ClassLoader劈愚,這些類加載器需要由啟動類加載器加載到內(nèi)存中之后才能去加載其他的類瞳遍。
站在開發(fā)者的角度來講,類加載器可以大致分為三類:
- 啟動類加載器:Bootstrap ClassLoader菌羽,與上面的相同掠械,它復(fù)雜加載存放在JDK/jre/lib下,或者被
-Xbootclasspatch
參數(shù)指定的路徑中的注祖,并且能被虛擬機識別的類庫(如rt.jar猾蒂,所有java*開頭的類均被Bootstrap ClassLoader加載)。啟動類加載器是無法被Java程序直接飲用的是晨。 - 擴展類加載器:Extension ClassLoader肚菠,該加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負(fù)責(zé)加載JDK/jre/lib/ext目錄中罩缴,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(如javax.*開頭的類)蚊逢,開發(fā)者可以直接使用擴展類加載器层扶。
- 應(yīng)用程序類加載器:Application ClassLoader,該加載器由sun.misc.Launcher$AppClassLoader來實現(xiàn)烙荷,它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類镜会,開發(fā)者可以直接使用該類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器终抽,一般情況下這個就是程序中磨人的加載器稚叹。
應(yīng)用程序性都是由這三類加載器互相配合進行加載的,如果有必要拿诸,我們還可以加入自定義的類加載器。因為JVM自帶的ClassLoader只是懂得從本地文件系統(tǒng)加載標(biāo)準(zhǔn)的java class文件塞茅,如果編寫了自己的ClassLoader亩码,可以做到如下幾點:
1)在執(zhí)行非置信代碼之前,自動驗證數(shù)字簽名
2)動態(tài)地創(chuàng)建符合用戶特定需要的定制化構(gòu)建類
3)從特定的場景取得java class野瘦,例如數(shù)據(jù)庫中和網(wǎng)絡(luò)中
事實上當(dāng)使用Applet的時候描沟,就用到了特定的ClassLoader,因為這時需要從網(wǎng)絡(luò)上加載java class鞭光,并且要檢查相關(guān)的安全信息吏廉,應(yīng)用服務(wù)器也大都是用了自定義的ClassLoader技術(shù)。
這種層次關(guān)系成為類加載器的雙親委派模型惰许。我們把每一層上面的類加載器叫做當(dāng)前層類加載器的父加載器席覆。但是,他們之間的斧子關(guān)系不是通過繼承來實現(xiàn)的汹买,二十使用組合關(guān)系來復(fù)用父加載器中的代碼佩伤。該模型在JDK1.2期間被引入并且廣泛應(yīng)用于之后的幾乎所有的Java程序中,但它并不是一個強制性約束模型晦毙,而是Java設(shè)計者們推薦給開發(fā)者的一種類加載器的實現(xiàn)方式生巡。
雙親委派模式的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類见妒,而是把請求委托給父加載器去完成孤荣,依次想上,因此须揣,所有類加載的請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中盐股,只有當(dāng)父加載器在它的搜索范圍中沒有找到所需的類時,子類加載器才會嘗試自己去加載該類返敬。
使用雙親委派模型來組織類加載器回見的關(guān)系遂庄,有一個明顯的好處,就是Java類隨著它的類加載器(說白了劲赠,就是它所在的目錄)一起舉杯了一種帶有優(yōu)先級的層次關(guān)系涛目,這對于保證Java程序的穩(wěn)定性運行很重要秸谢。例如,類java.lang.Objec類存放在JDK/jre/lib下的rt.jar中霹肝,因此無論是哪個類加載器要加載這個類估蹄,最終都會委派給啟動類加載器進行加載。這便保證了Object類在程序中的各種類加載器中都是同一個類沫换。
驗證
驗證的目的是為了確保Class文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機的要求臭蚁,而且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現(xiàn)可能會有所不同讯赏,但大致都會完成以下四個階段的驗證:文件格式的驗證垮兑、元數(shù)據(jù)的驗證、字節(jié)碼驗證和符號引用驗證漱挎。
- 文件格式的驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范系枪,并且能被當(dāng)前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲與方法區(qū)之內(nèi)磕谅。經(jīng)過該階段的驗證后私爷,字節(jié)流才會進入內(nèi)存的方區(qū)中進行存儲,后面的三個驗證都是基于方法區(qū)的存儲結(jié)構(gòu)進行的膊夹。
- 元數(shù)據(jù)驗證:對類的元數(shù)據(jù)信息進行語義校驗(其實就是對類中的各種數(shù)據(jù)類型進行語法校驗)衬浑,保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息。
- 字節(jié)碼驗證:該階段驗證主要工作是進行數(shù)據(jù)流和控制流分析放刨,對類的方法體進行校驗分析工秩,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。
- 符號引用驗證:這是最后一個階段的驗證进统,它發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候(解析階段中發(fā)生該轉(zhuǎn)化拓诸,后面會有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗麻昼。
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段奠支,這些內(nèi)存都將在方法區(qū)中分配。對于該階段有以下幾點需要注意:
- 這時候進行內(nèi)存分配的僅包括類變量(static)抚芦,而不包括實例變量倍谜,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
- 這里所設(shè)置的初始化值通常情況下是數(shù)據(jù)類型默認(rèn)的0值(如0叉抡、0L尔崔、null、false等)褥民,而不是在Java代碼中被顯式賦予的值季春。
假設(shè)一個類變量的定義為:public static int value = 3
那么變量value在準(zhǔn)備階段過后的初始化值為0,而不是3消返,因為這時候尚未開始執(zhí)行任何Java方法载弄,而把value賦值為3的putstatic指定是在程序編譯后耘拇,存放與類構(gòu)造器<clinit>()方法之中的,所以把value賦值為3的動作將在初始化階段才會執(zhí)行宇攻。
下表列出了Java中所有基本數(shù)據(jù)類型以及reference類型的默認(rèn)零值惫叛。
數(shù)據(jù)類型 | 默認(rèn)零值 |
---|---|
int | 0 |
long | 0L |
short | (short) 0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
- 如果類型字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾逞刷,那么在準(zhǔn)備階段變量value就會初始化為ConstaltValue實行所指定的值嘉涌。
假設(shè)上面的類變量value被定義為:public static final int value = 3;
編譯時Javac將會生成ConstantValue屬性,在準(zhǔn)備階段虛擬機就會根據(jù)ConstantValue的設(shè)置將value賦值為3夸浅。我們可以理解為static final常量在編譯期就將其結(jié)果放入了調(diào)用它的類常量池中仑最。
解析
解析階段是虛擬機將常量池中的符號引用轉(zhuǎn)化為直接引用的過程。
符號引用和直接引用的區(qū)別與關(guān)聯(lián):
- 符號引用:符號引用以一組符號來描述所引用的目標(biāo)帆喇,符號可以是任何形式的字面量词身,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)番枚,引用的目標(biāo)并不一定已經(jīng)加載到了內(nèi)存中。
- 直接引用:直接引用可以是直接指向目標(biāo)的指針损敷、相對偏移量或是一個能簡介定位到目標(biāo)的句柄葫笼。直接引用是與虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機實現(xiàn)上翻譯出來的直接引用一般會不同拗馒。如果有了直接引用路星,那說明引用的目標(biāo)必定已經(jīng)在內(nèi)存中了。
解析階段可能開始于初始化之前诱桂,也可能在初始化之后開始洋丐,虛擬機會根據(jù)需要來判斷,到底是在類被加載時就對常量池中的符號引用進行解析(初始化之前)挥等,還是等到一個符號引用將要被使用前才去解析它(初始化之后)友绝。
對同一個符號引用進行多次解析請求是很常見的事情,虛擬機實現(xiàn)可能會對第一次解析的結(jié)果進行緩存(在運行時常量池中記錄直接引用肝劲,并把創(chuàng)兩標(biāo)示為解析狀態(tài))迁客,從而避免解析動作重復(fù)進行。
解析動作主要針對類或接口辞槐、字段掷漱、類方法、接口方法四類符號引用進行榄檬,分別對應(yīng)于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info藕甩、CONSTANT_InterfaceMethodref_info
四種常量類型。
-
類或接口的解析:判斷所要轉(zhuǎn)化成直接引用是對數(shù)組類型锦爵,還是普通的對象類型的引用,從而進行不同的解析喳魏。
2.字段解析:對字段進行解析時棉浸,會先在奔雷中查找是否包含有簡單名稱和字段描述符都與目標(biāo)相匹配的字段,如果有刺彩,則查找結(jié)束迷郑;如果沒有,則按照繼承關(guān)系從上往下遞歸搜索該類所實現(xiàn)的各個接口和他們的父接口创倔,還沒有嗡害,則按照繼承關(guān)系從上往下遞歸搜索其父類,知道查找結(jié)束畦攘,查找流程如下圖所示:
從下面一段代碼的執(zhí)行結(jié)果中很容易看出來字段解析的搜索順序:
class Super{
public static int m = 11;
static{
System.out.println("執(zhí)行了super類靜態(tài)語句塊");
}
}
class Father extends Super{
public static int m = 33;
static{
System.out.println("執(zhí)行了父類靜態(tài)語句塊");
}
}
class Child extends Father{
static{
System.out.println("執(zhí)行了子類靜態(tài)語句塊");
}
}
public class StaticTest{
public static void main(String[] args){
System.out.println(Child.m);
}
}
執(zhí)行結(jié)果如下
執(zhí)行了super類靜態(tài)語句塊
執(zhí)行了父類靜態(tài)語句塊
33
如果注釋掉Father類中對m定義的那一行霸妹,則輸出結(jié)果如下:
執(zhí)行了super類靜態(tài)語句塊
11
static變量發(fā)生在靜態(tài)解析階段,也即是初始化之前知押,此時已經(jīng)將字段的符號引用轉(zhuǎn)換為了內(nèi)存引用叹螟,也便將它與對應(yīng)的類關(guān)聯(lián)在了一起,由于在子類中沒有查找到與m想匹配的字段台盯,那么m便不會與子類關(guān)聯(lián)在一起罢绽,因此并不會觸發(fā)子類的初始化。
最后需要注意:理論上是按照上述順序進行搜索解析静盅,但在實際應(yīng)用中良价,虛擬機的編譯器可能要比上述規(guī)范要求的更嚴(yán)格一些。如果有一個同名字段同時出現(xiàn)在該類的接口和父類中蒿叠,或同時在自己或父類的接口中出現(xiàn)明垢,編譯器可能會拒絕編譯。
初始化
初始化是類加載過程的最后一步市咽,到了這個階段痊银,才真正開始執(zhí)行類中定義的Java程序代碼。在準(zhǔn)備階段施绎,類變量已經(jīng)被賦過一次系統(tǒng)要求的初始值曼验,而在初始化階段,則是根據(jù)程序員通過程序制定的主管計劃去初始化變量和其他資源粘姜,或者可以從另一個角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程鬓照。
這里簡單說明下<clinit>()方法的執(zhí)行規(guī)則:
-
<clinit>()
方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的孤紧,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量豺裆,定義在它之后的變量,在前面的靜態(tài)語句中可以賦值,但是不能訪問臭猜。 -
<clinit>()
方法與實例構(gòu)造器<init>()
方法(類的構(gòu)造函數(shù))不同躺酒,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機會保證在子類<clinit>()
方法執(zhí)行之前蔑歌,父類<clinit>()
方法已經(jīng)執(zhí)行完畢羹应。因此,在虛擬機中第一個被執(zhí)行的<clinit>()
方法的類肯定是java.lang.Object
- <clinit>()方法對于類或接口來說并不是必須的次屠,如果一個類中沒有靜態(tài)語句塊园匹,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法劫灶。
- 接口中不能使用靜態(tài)語句塊裸违,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會生成<clinit>()方法本昏。但是接口魚類不同的是:執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法供汛,只有當(dāng)父接口中定義的變量被使用時,父接口才會被初始化涌穆。另外怔昨,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法。
- 虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確地加鎖和同步宿稀,如果多個線程同時去初始化一個類趁舀,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法,其他線程都需要阻塞等待原叮,直到活動線程執(zhí)行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作巡蘸,那就可能造成多個線程阻塞奋隶,在實際應(yīng)用中這種阻塞往往是很隱蔽的。
下面給出一個簡單的例子悦荒,以便更清晰地說明如上規(guī)則:
class Father{
public static int a = 1;
static{
a = 2;
}
}
class Child extends Father{
public static int b = a;
}
public class ClinitTest{
public static void main(String[] args){
System.out.println(Child.b);
}
}
執(zhí)行上面的代碼唯欣,會打印出2,也就是說b的值被賦為了2
我們來看得到該結(jié)果的步驟搬味。首先在準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置類變量初始值境氢,這樣A和B均被賦值為默認(rèn)值0,而后再在調(diào)用<clinit>()方法時給他們賦予程序中指定的值碰纬。當(dāng)我們調(diào)用Child.b時萍聊,觸發(fā)Child的<clinit>()方法,根據(jù)規(guī)則2悦析,在此之前寿桨,要先執(zhí)行完其父類Father的<clinit>()方法,又根據(jù)規(guī)則1强戴,在執(zhí)行<clinit>()方法時亭螟,需要按static語句或static變量賦值操作等在代碼中出現(xiàn)的順序來執(zhí)行相關(guān)的static語句挡鞍,因此當(dāng)觸發(fā)執(zhí)行Father的<clinit>()方法時,會先將a賦值為1预烙,再執(zhí)行static語句塊中語句墨微,將a賦值為2,而后再執(zhí)行Child類的<clinit>()方法扁掸,這樣便會將b的賦值為2.
如果我們顛倒一下Father類中“public static int a = 1;”語句和“static語句塊”的順序翘县,程序執(zhí)行后,則會打印出1也糊。
很明顯是根據(jù)規(guī)則1炼蹦,執(zhí)行Father的<clinit>()
方法時,根據(jù)順序先執(zhí)行了static語句塊中的內(nèi)容狸剃,后執(zhí)行了public static int a = 1;
語句掐隐。
另外,在顛倒二者的順序之后钞馁,如果在static語句塊中對a進行訪問(比如將a賦給某個變量)虑省,在編譯時將會報錯,因為根據(jù)規(guī)則1僧凰,它只能對a進行賦值探颈,而不能訪問。
總結(jié)
整個類加載過程中训措,除了在加載階段用戶應(yīng)用程序可以自定義類加載器參與之外伪节,其余所有的動作完全由虛擬機主導(dǎo)和控制。到了初始化才開始執(zhí)行類中定義的Java程序代碼(亦及字節(jié)碼)绩鸣,但這里的執(zhí)行代碼只是個開端怀大,它僅限于<clinit>()
方法。類加載過程中主要是將Class文件(準(zhǔn)確地講呀闻,應(yīng)該是類的二進制字節(jié)流)加載到虛擬機內(nèi)存中化借,真正執(zhí)行字節(jié)碼的操作,在加載完成后才真正開始捡多。