https://www.cnblogs.com/xrq730/p/4844915.html
前言
我們知道我們寫的程序經(jīng)過(guò)編譯后成為了.class文件脊框,.class文件中描述了類的各種信息,最終都需要加載到虛擬機(jī)之后才能運(yùn)行和使用。而虛擬機(jī)如何加載這些.class文件涕蜂?.class文件的信息進(jìn)入到虛擬機(jī)后會(huì)發(fā)生什么變化?這些都是本文要講的內(nèi)容蠕趁,文章將會(huì)講解加載類加載的每個(gè)階段Java虛擬機(jī)需要做什么事(加粗標(biāo)紅)垃它。
類使用的7個(gè)階段
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存肉盹,它的整個(gè)生命周期包括:加載(Loading)昔驱、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)上忍、解析(Resolution)骤肛、初始化(Initiallization)、使用(Using)和卸載(Unloading)這7個(gè)階段窍蓝。其中驗(yàn)證腋颠、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接(Linking)吓笙,這七個(gè)階段的發(fā)生順序如下圖:
圖中淑玫,加載、驗(yàn)證、準(zhǔn)備絮蒿、初始化尊搬、卸載這5個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這種順序按部就班地開始土涝,而解析階段不一定:它在某些情況下可以初始化階段之后在開始毁嗦,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定)。接下來(lái)講解加載回铛、驗(yàn)證狗准、準(zhǔn)備、解析茵肃、初始化五個(gè)步驟腔长,這五個(gè)步驟組成了一個(gè)完整的類加載過(guò)程。使用沒(méi)什么好說(shuō)的验残,卸載屬于GC的工作捞附,在之前GC的文章中已經(jīng)有所提及了。
加載Loading
加載是類加載的第一個(gè)階段您没。有兩種時(shí)機(jī)會(huì)觸發(fā)類加載:
1鸟召、預(yù)加載。虛擬機(jī)啟動(dòng)時(shí)加載氨鹏,加載的是JAVA_HOME/lib/下的rt.jar下的.class文件欧募,這個(gè)jar包里面的內(nèi)容是程序運(yùn)行時(shí)非常常常用到的,像java.lang.*仆抵、java.util.*跟继、java.io.*等等,因此隨著虛擬機(jī)一起加載镣丑。要證明這一點(diǎn)很簡(jiǎn)單舔糖,寫一個(gè)空的main函數(shù),設(shè)置虛擬機(jī)參數(shù)為"-XX:+TraceClassLoading"來(lái)獲取類加載信息莺匠,運(yùn)行一下:
1[Opened E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 2[Loaded java.lang.Object from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 3[Loaded java.io.Serializable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 4[Loaded java.lang.Comparable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 5[Loaded java.lang.CharSequence from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 6[Loaded java.lang.String from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 7[Loaded java.lang.reflect.GenericDeclaration from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 8[Loaded java.lang.reflect.Type from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 9[Loaded java.lang.reflect.AnnotatedElement from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]10[Loaded java.lang.Class from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]11[Loaded java.lang.Cloneable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]12...
2金吗、運(yùn)行時(shí)加載。虛擬機(jī)在用到一個(gè).class文件的時(shí)候趣竣,會(huì)先去內(nèi)存中查看一下這個(gè).class文件有沒(méi)有被加載摇庙,如果沒(méi)有就會(huì)按照類的全限定名來(lái)加載這個(gè)類。
那么期贫,加載階段做了什么跟匆,其實(shí)加載階段做了有三件事情:
獲取.class文件的二進(jìn)制流
將類信息异袄、靜態(tài)變量通砍、字節(jié)碼、常量這些.class文件中的內(nèi)容放入方法區(qū)中
在內(nèi)存中生成一個(gè)代表這個(gè).class文件的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口封孙。一般這個(gè)Class是在堆里的迹冤,不過(guò)HotSpot虛擬機(jī)比較特殊,這個(gè)Class對(duì)象是放在方法區(qū)中的
虛擬機(jī)規(guī)范對(duì)這三點(diǎn)的要求并不具體虎忌,因此虛擬機(jī)實(shí)現(xiàn)與具體應(yīng)用的靈活度都是相當(dāng)大的泡徙。例如第一條,根本沒(méi)有指明二進(jìn)制字節(jié)流要從哪里來(lái)膜蠢、怎么來(lái)堪藐,因此單單就這一條,就能變出許多花樣來(lái):
從zip包中獲取挑围,這就是以后jar礁竞、ear、war格式的基礎(chǔ)
從網(wǎng)絡(luò)中獲取杉辙,典型應(yīng)用就是Applet
運(yùn)行時(shí)計(jì)算生成模捂,典型應(yīng)用就是動(dòng)態(tài)代理技術(shù)
由其他文件生成,典型應(yīng)用就是JSP蜘矢,即由JSP生成對(duì)應(yīng)的.class文件
從數(shù)據(jù)庫(kù)中讀取狂男,這種場(chǎng)景比較少見(jiàn)
總而言之,在類加載整個(gè)過(guò)程中品腹,這部分是對(duì)于開發(fā)者來(lái)說(shuō)可控性最強(qiáng)的一個(gè)階段岖食。
驗(yàn)證
連接階段的第一步,這一階段的目的是為了確保.class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求舞吭,并且不會(huì)危害虛擬機(jī)自身的安全县耽。
Java語(yǔ)言本身是相對(duì)安全的語(yǔ)言(相對(duì)C/C++來(lái)說(shuō)),但是前面說(shuō)過(guò)镣典,.class文件未必要從Java源碼編譯而來(lái)兔毙,可以使用任何途徑產(chǎn)生,甚至包括用十六進(jìn)制編輯器直接編寫來(lái)產(chǎn)生.class文件兄春。在字節(jié)碼語(yǔ)言層面上澎剥,Java代碼至少?gòu)恼Z(yǔ)義上是可以表達(dá)出來(lái)的。虛擬機(jī)如果不檢查輸入的字節(jié)流赶舆,對(duì)其完全信任的話哑姚,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對(duì)自身保護(hù)的一項(xiàng)重要工作芜茵。
驗(yàn)證階段將做一下幾個(gè)工作叙量,具體就不細(xì)講了,這是虛擬機(jī)實(shí)現(xiàn)層面的問(wèn)題:
文件格式驗(yàn)證
這個(gè)地方要說(shuō)一點(diǎn)和開發(fā)者相關(guān)的九串。.class文件的第5~第8個(gè)字節(jié)表示的是該.class文件的主次版本號(hào)绞佩,驗(yàn)證的時(shí)候會(huì)對(duì)這4個(gè)字節(jié)做一個(gè)驗(yàn)證寺鸥,高版本的JDK能向下兼容以前版本的.class文件,但不能運(yùn)行以后的class文件品山,即使文件格式未發(fā)生任何變化胆建,虛擬機(jī)也必須拒絕執(zhí)行超過(guò)其版本號(hào)的.class文件。舉個(gè)具體的例子肘交,如果一段.java代碼是在JDK1.6下編譯的笆载,那么JDK1.6、JDK1.7的環(huán)境能運(yùn)行這個(gè).java代碼生成的.class文件涯呻,但是JDK1.5凉驻、JDK1.4乃更低的JDK版本是無(wú)法運(yùn)行這個(gè).java代碼生成的.class文件的。如果運(yùn)行复罐,會(huì)拋出java.lang.UnsupportedClassVersionError沿侈,這個(gè)小細(xì)節(jié),務(wù)必注意市栗。
元數(shù)據(jù)驗(yàn)證
字節(jié)碼驗(yàn)證
符號(hào)引用驗(yàn)證
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置其初始值的階段缀拭,這些變量所使用的內(nèi)存都將在方法區(qū)中分配。關(guān)于這點(diǎn)填帽,有兩個(gè)地方注意一下:
這時(shí)候進(jìn)行內(nèi)存分配的僅僅是類變量(被static修飾的變量)蛛淋,而不是實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化的時(shí)候隨著對(duì)象一起分配在Java堆中
這個(gè)階段賦初始值的變量指的是那些不被final修飾的static變量篡腌,比如"public static int value = 123;"褐荷,value在準(zhǔn)備階段過(guò)后是0而不是123,給value賦值為123的動(dòng)作將在初始化階段才進(jìn)行嘹悼;比如"public static final int value = 123;"就不一樣了叛甫,在準(zhǔn)備階段,虛擬機(jī)就會(huì)給value賦值為123杨伙。
各個(gè)數(shù)據(jù)類型的零值如下圖:
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程其监。來(lái)了解一下符號(hào)引用和直接引用有什么區(qū)別:
1、符號(hào)引用限匣。
這個(gè)其實(shí)是屬于編譯原理方面的概念抖苦,符號(hào)引用包括了下面三類常量:
類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符
這么說(shuō)可能不太好理解,結(jié)合實(shí)際看一下米死,寫一段很簡(jiǎn)單的代碼:
1package com.xrq.test6; 2 3publicclass TestMain 4{ 5privatestaticint i; 6privatedouble d; 7 8publicstaticvoid print() 9? ? {1011? ? }1213privateboolean trueOrFalse()14? ? {15returnfalse;16? ? }17}
用javap把這段代碼的.class反編譯一下:
Constant pool:
? #1 = Class? ? ? ? ? ? ? #2//? com/xrq/test6/TestMain#2 = Utf8? ? ? ? ? ? ? com/xrq/test6/TestMain
? #3 = Class? ? ? ? ? ? ? #4//? java/lang/Object#4 = Utf8? ? ? ? ? ? ? java/lang/Object
? #5 = Utf8? ? ? ? ? ? ? i
? #6 = Utf8? ? ? ? ? ? ? I
? #7 = Utf8? ? ? ? ? ? ? d
? #8 = Utf8? ? ? ? ? ? ? D
? #9 = Utf8? ? ? ? ? ? ? ? #10 = Utf8? ? ? ? ? ? ? ()V
? #11 = Utf8? ? ? ? ? ? ? Code
? #12 = Methodref? ? ? ? ? #3.#13//? java/lang/Object."<init>":()V#13 = NameAndType? ? ? ? #9:#10//? "<init>":()V#14 = Utf8? ? ? ? ? ? ? LineNumberTable
? #15 = Utf8? ? ? ? ? ? ? LocalVariableTable
? #16 = Utf8this? #17 = Utf8? ? ? ? ? ? ? Lcom/xrq/test6/TestMain;
? #18 = Utf8? ? ? ? ? ? ? print
? #19 = Utf8? ? ? ? ? ? ? trueOrFalse
? #20 = Utf8? ? ? ? ? ? ? ()Z
? #21 = Utf8? ? ? ? ? ? ? SourceFile
? #22 = Utf8? ? ? ? ? ? ? TestMain.java
看到Constant Pool也就是常量池中有22項(xiàng)內(nèi)容锌历,其中帶"Utf8"的就是符號(hào)引用。比如#2峦筒,它的值是"com/xrq/test6/TestMain"究西,表示的是這個(gè)類的全限定名;又比如#5為i物喷,#6為I卤材,它們是一對(duì)的遮斥,表示變量時(shí)Integer(int)類型的,名字叫做i商膊;#6為D伏伐、#7為d也是一樣宠进,表示一個(gè)Double(double)類型的變量晕拆,名字為d;#18材蹬、#19表示的都是方法的名字实幕。
那其實(shí)總而言之,符號(hào)引用和我們上面講的是一樣的堤器,是對(duì)于類昆庇、變量、方法的描述闸溃。符號(hào)引用和虛擬機(jī)的內(nèi)存布局是沒(méi)有關(guān)系的整吆,引用的目標(biāo)未必已經(jīng)加載到內(nèi)存中了。
2辉川、直接引用
直接引用可以是直接指向目標(biāo)的指針表蝙、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的乓旗,同一個(gè)符號(hào)引用在不同的虛擬機(jī)示例上翻譯出來(lái)的直接引用一般不會(huì)相同府蛇。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)存在在內(nèi)存中了屿愚。
初始化
初始化階段是類加載過(guò)程的最后一步汇跨,初始化階段是真正執(zhí)行類中定義的Java程序代碼(或者說(shuō)是字節(jié)碼)的過(guò)程。初始化過(guò)程是一個(gè)執(zhí)行類構(gòu)造器()方法的過(guò)程妆距,根據(jù)程序員通過(guò)程序制定的主觀計(jì)劃去初始化類變量和其它資源穷遂。把這句話說(shuō)白一點(diǎn),其實(shí)初始化階段做的事就是給static變量賦予用戶指定的值以及執(zhí)行靜態(tài)代碼塊娱据。
注意一下塞颁,虛擬機(jī)會(huì)保證類的初始化在多線程環(huán)境中被正確地加鎖、同步吸耿,即如果多個(gè)線程同時(shí)去初始化一個(gè)類祠锣,那么只會(huì)有一個(gè)類去執(zhí)行這個(gè)類的()方法,其他線程都要阻塞等待咽安,直至活動(dòng)線程執(zhí)行()方法完畢伴网。因此如果在一個(gè)類的()方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程阻塞妆棒。不過(guò)其他線程雖然會(huì)阻塞澡腾,但是執(zhí)行()方法的那條線程退出()方法后沸伏,其他線程不會(huì)再次進(jìn)入()方法了,因?yàn)橥粋€(gè)類加載器下动分,一個(gè)類只會(huì)初始化一次毅糟。實(shí)際應(yīng)用中這種阻塞往往是比較隱蔽的,要小心澜公。
Java虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有5種場(chǎng)景必須立即對(duì)類進(jìn)行初始化姆另,這4種場(chǎng)景也稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用(其實(shí)還有一種場(chǎng)景,不過(guò)暫時(shí)我還沒(méi)弄明白這種場(chǎng)景的意思坟乾,就先不寫了):
使用new關(guān)鍵字實(shí)例化對(duì)象迹辐、讀取或者設(shè)置一個(gè)類的靜態(tài)字段(被final修飾的靜態(tài)字段除外)、調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候
使用java.lang.reflect包中的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候
初始化一個(gè)類甚侣,發(fā)現(xiàn)其父類還沒(méi)有初始化過(guò)的時(shí)候
虛擬機(jī)啟動(dòng)的時(shí)候明吩,虛擬機(jī)會(huì)先初始化用戶指定的包含main()方法的那個(gè)類
除了上面4種場(chǎng)景外,所有引用類的方式都不會(huì)觸發(fā)類的初始化殷费,稱為被動(dòng)引用印荔,接下來(lái)看下被動(dòng)引用的幾個(gè)例子:
1、子類引用父類靜態(tài)字段详羡,不會(huì)導(dǎo)致子類初始化仍律。至于子類是否被加載、驗(yàn)證了殷绍,前者可以通過(guò)"-XX:+TraceClassLoading"來(lái)查看
publicclass SuperClass
{
? ? publicstaticintvalue = 123;
? ? static? ? {
? ? ? ? System.out.println("SuperClass init");
? ? }
}publicclassSubClassextends SuperClass
{
? ? static? ? {
? ? ? ? System.out.println("SubClass init");
? ? }
}publicclass TestMain
{
? ? publicstaticvoid main(String[] args)
? ? {
? ? ? ? System.out.println(SubClass.value);
? ? }
}
運(yùn)行結(jié)果為
SuperClass init123
2染苛、通過(guò)數(shù)組定義引用類,不會(huì)觸發(fā)此類的初始化
publicclass SuperClass
{
? ? publicstaticintvalue = 123;
? ? static? ? {
? ? ? ? System.out.println("SuperClass init");
? ? }
}publicclass TestMain
{
? ? publicstaticvoid main(String[] args)
? ? {
? ? ? ? SuperClass[] scs =newSuperClass[10];
? ? }
}
運(yùn)行結(jié)果為
3主到、引用靜態(tài)常量時(shí)茶行,常量在編譯階段會(huì)存入類的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類
publicclass ConstClass
{
? ? publicstaticfinalString HELLOWORLD =? "Hello World";
? ? static? ? {
? ? ? ? System.out.println("ConstCLass init");
? ? }
}publicclass TestMain
{
? ? publicstaticvoid main(String[] args)
? ? {
? ? ? ? System.out.println(ConstClass.HELLOWORLD);
? ? }
}
運(yùn)行結(jié)果為
Hello World
在編譯階段通過(guò)常量傳播優(yōu)化登钥,常量HELLOWORLD的值"Hello World"實(shí)際上已經(jīng)存儲(chǔ)到了NotInitialization類的常量池中畔师,以后NotInitialization對(duì)常量ConstClass.HELLOWORLD的引用實(shí)際上都被轉(zhuǎn)化為NotInitialization類對(duì)自身常量池的引用了。也就是說(shuō)牧牢,實(shí)際上的NotInitialization的Class文件中并沒(méi)有ConstClass類的符號(hào)引用入口看锉,這兩個(gè)類在編譯成Class之后就不存在任何聯(lián)系了。