Java类加载的那些事
轉(zhuǎn)載自?Java類加載的那些事
?
前言
Java源代碼被編譯成class字節(jié)碼,最終需要加載到虛擬機(jī)中才能運(yùn)行。整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段。?
加載
1、通過一個(gè)類的全限定名獲取描述此類的二進(jìn)制字節(jié)流;?
2、將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)保存為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);?
3、在java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為訪問方法區(qū)的入口;
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把加載動(dòng)作放到JVM外部實(shí)現(xiàn),以便讓應(yīng)用程序決定如何獲取所需的類,實(shí)現(xiàn)這個(gè)動(dòng)作的代碼稱為“類加載器”,JVM提供了3種類加載器:?
1、啟動(dòng)類加載器(Bootstrap ClassLoader):負(fù)責(zé)加載 JAVAHOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機(jī)認(rèn)可(按文件名識(shí)別,如rt.jar)的類。?
2、擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)加載 JAVAHOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫(kù)。?
3、應(yīng)用程序類加載器(Application ClassLoader):負(fù)責(zé)加載用戶路徑(classpath)上的類庫(kù)。
JVM基于上述類加載器,通過雙親委派模型進(jìn)行類的加載,當(dāng)然我們也可以通過繼承java.lang.ClassLoader實(shí)現(xiàn)自定義的類加載器。
?
雙親委派模型工作過程:當(dāng)一個(gè)類加載器收到類加載任務(wù),優(yōu)先交給其父類加載器去完成,因此最終加載任務(wù)都會(huì)傳遞到頂層的啟動(dòng)類加載器,只有當(dāng)父類加載器無法完成加載任務(wù)時(shí),才會(huì)嘗試執(zhí)行加載任務(wù)。
雙親委派模型有什么好處? 比如位于rt.jar包中的類java.lang.Object,無論哪個(gè)加載器加載這個(gè)類,最終都是委托給頂層的啟動(dòng)類加載器進(jìn)行加載,確保了Object類在各種加載器環(huán)境中都是同一個(gè)類。
驗(yàn)證
為了確保Class文件符合當(dāng)前虛擬機(jī)要求,需要對(duì)其字節(jié)流數(shù)據(jù)進(jìn)行驗(yàn)證,主要包括格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。?
格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合class文件格式的規(guī)范,并且能被當(dāng)前虛擬機(jī)處理,如是否以魔數(shù)0xCAFEBABE開頭、主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍內(nèi)、常量池是否有不支持的常量類型等。只有經(jīng)過格式驗(yàn)證的字節(jié)流,才會(huì)存儲(chǔ)到方法區(qū)的數(shù)據(jù)結(jié)構(gòu),剩余3個(gè)驗(yàn)證都基于方法區(qū)的數(shù)據(jù)進(jìn)行。
元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的數(shù)據(jù)進(jìn)行語義分析,以保證符合Java語言規(guī)范,如是否繼承了final修飾的類、是否實(shí)現(xiàn)了父類的抽象方法、是否覆蓋了父類的final方法或final字段等。
字節(jié)碼驗(yàn)證:對(duì)類的方法體進(jìn)行分析,確保在方法運(yùn)行時(shí)不會(huì)有危害虛擬機(jī)的事件發(fā)生,如保證操作數(shù)棧的數(shù)據(jù)類型和指令代碼序列的匹配、保證跳轉(zhuǎn)指令的正確性、保證類型轉(zhuǎn)換的有效性等。
符號(hào)引用驗(yàn)證:為了確保后續(xù)的解析動(dòng)作能夠正常執(zhí)行,對(duì)符號(hào)引用進(jìn)行驗(yàn)證,如通過字符串描述的全限定名是都能找到對(duì)應(yīng)的類、在指定類中是否存在符合方法的字段描述符等。
準(zhǔn)備
在準(zhǔn)備階段,為類變量(static修飾)在方法區(qū)中分配內(nèi)存并設(shè)置初始值。
private static int var = 100;準(zhǔn)備階段完成后,var 值為0,而不是100。在初始化階段,才會(huì)把100賦值給val,但是有個(gè)特殊情況:
private static final int VAL= 100;在編譯階段會(huì)為VAL生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)會(huì)根據(jù)ConstantValue屬性將VAL賦值為100。
解析
解析階段是將常量池中的符號(hào)引用替換為直接引用的過程,符號(hào)引用和直接引用有什么不同??
1、符號(hào)引用使用一組符號(hào)來描述所引用的目標(biāo),可以是任何形式的字面常量,定義在Class文件格式中。?
2、直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或則能間接定位到目標(biāo)的句柄。
初始化
初始化階段是執(zhí)行類構(gòu)造器方法的過程,方法由類變量的賦值動(dòng)作和靜態(tài)語句塊按照在源文件出現(xiàn)的順序合并而成,該合并操作由編譯器完成。
private static int value = 100;static int a = 100;static int b = 100;static int c;static {c = a + b;System.out.println("it only run once");}1、方法對(duì)于類或接口不是必須的,如果一個(gè)類中沒有靜態(tài)代碼塊,也沒有靜態(tài)變量的賦值操作,那么編譯器不會(huì)生成;?
2、方法與實(shí)例構(gòu)造器不同,不需要顯式的調(diào)用父類的方法,虛擬機(jī)會(huì)保證父類的優(yōu)先執(zhí)行;?
3、為了防止多次執(zhí)行,虛擬機(jī)會(huì)確保方法在多線程環(huán)境下被正確的加鎖同步執(zhí)行,如果有多個(gè)線程同時(shí)初始化一個(gè)類,那么只有一個(gè)線程能夠執(zhí)行方法,其它線程進(jìn)行阻塞等待,直到執(zhí)行完成。?
4、注意:執(zhí)行接口的方法不需要先執(zhí)行父接口的,只有使用父接口中定義的變量時(shí),才會(huì)執(zhí)行。
類初始化場(chǎng)景
虛擬機(jī)中嚴(yán)格規(guī)定了有且只有5種情況必須對(duì)類進(jìn)行初始化。?
1、執(zhí)行new、getstatic、putstatic和invokestatic指令;?
2、使用reflect對(duì)類進(jìn)行反射調(diào)用;?
3、初始化一個(gè)類的時(shí)候,父類還沒有初始化,會(huì)事先初始化父類;?
4、啟動(dòng)虛擬機(jī)時(shí),需要初始化包含main方法的類;?
5、在JDK1.7中,如果java.lang.invoke.MethodHandler實(shí)例最后的解析結(jié)果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類沒有進(jìn)行初始化;
以下幾種情況,不會(huì)觸發(fā)類初始化?
1、通過子類引用父類的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化。
class Parent {static int a = 100;static {System.out.println("parent init!");} }class Child extends Parent {static {System.out.println("child init!");} }public class Init{ public static void main(String[] args){ System.out.println(Child.a); } }輸出結(jié)果為: parent init! 100
2、定義對(duì)象數(shù)組,不會(huì)觸發(fā)該類的初始化。
public class Init{ public static void main(String[] args){ Parent[] parents = new Parent[10];} }無輸出,說明沒有觸發(fā)類Parent的初始化,但是這段代碼做了什么?先看看生成的字節(jié)碼指令
?anewarray指令為新數(shù)組分配空間,并觸發(fā)[Lcom.ctrip.ttd.whywhy.Parent類的初始化,這個(gè)類由虛擬機(jī)自動(dòng)生成。
3、常量在編譯期間會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會(huì)觸發(fā)定義常量所在的類。
class Const {static final int A = 100;static {System.out.println("Const init");} }public class Init{ public static void main(String[] args){ System.out.println(Const.A); } }輸出: 100?
說明沒有觸發(fā)類Const的初始化,在編譯階段,Const類中常量A的值100存儲(chǔ)到Init類的常量池中,這兩個(gè)類在編譯成class文件之后就沒有聯(lián)系了。
4、通過類名獲取Class對(duì)象,不會(huì)觸發(fā)類的初始化。
public class test {public static void main(String[] args) throws ClassNotFoundException {Class c_dog = Dog.class;Class clazz = Class.forName("zzzzzz.Cat");} }class Cat {private String name;private int age;static {System.out.println("Cat is load");} }class Dog {private String name;private int age;static {System.out.println("Dog is load");}執(zhí)行結(jié)果:Cat is load
所以通過Dog.class并不會(huì)觸發(fā)Dog類的初始化動(dòng)作。
5、通過Class.forName加載指定類時(shí),如果指定參數(shù)initialize為false時(shí),也不會(huì)觸發(fā)類初始化,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對(duì)類進(jìn)行初始化。
public class test {public static void main(String[] args) throws ClassNotFoundException {Class clazz = Class.forName("zzzzzz.Cat", false, Cat.class.getClassLoader());} } class Cat {private String name;private int age;static {System.out.println("Cat is load");} }6、通過ClassLoader默認(rèn)的loadClass方法,也不會(huì)觸發(fā)初始化動(dòng)作
new ClassLoader(){}.loadClass("zzzzzz.Cat");?
?
?
總結(jié)
以上是生活随笔為你收集整理的Java类加载的那些事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 性价比最高的电脑配置(最高的电脑配置)
- 下一篇: Java读取properties配置文件