轉(zhuǎn)載請(qǐng)說(shuō)明出處:Java面試相關(guān)(一)-- Java類(lèi)加載全過(guò)程
概述:
我們知道,Java中我們寫(xiě)類(lèi)的代碼艾杏,是存在于一個(gè)個(gè).java文件中的,而這個(gè)后綴名也是讓JVM識(shí)別編譯的基礎(chǔ)絮吵】螅可能有些Android開(kāi)發(fā)者對(duì)幾個(gè)ClassLoader(如:AppClassLoader等)比較熟悉,那么漓帚,整個(gè)類(lèi)的加載過(guò)程:從未進(jìn)行編譯的.java文件母债,到類(lèi)的初始化完畢并等待被實(shí)例化使用的過(guò)程,具體是怎么樣的尝抖。
??收集Java資料毡们、看了幾篇本人覺(jué)得較好的博文后,總結(jié)以下關(guān)于Java類(lèi)的加載過(guò)程昧辽,掌握此過(guò)程衙熔,能夠更加理解Java類(lèi)的各個(gè)方法的執(zhí)行順序,以及JVM的工作和Java類(lèi)生成的原理搅荞。讀者笑納~
類(lèi)的加載過(guò)程分析
類(lèi)從.java文件到實(shí)際加載到內(nèi)存中红氯,實(shí)際上是這樣的一個(gè)過(guò)程:
??.java文件 -> 通過(guò)你的JDK環(huán)境相關(guān)指令編譯 -> .class文件 -> JVM初始化之后,如果有類(lèi)的執(zhí)行咕痛、調(diào)用等相關(guān)操作脖隶,JVM就會(huì)將.class文件加載到內(nèi)存中,并開(kāi)始下面的一系列處理:(鏈接->初始化)
一暇检、關(guān)于ClassLoader
首先我們要搞清楚一點(diǎn)产阱,ClassLoader是Java用于加載類(lèi)的一個(gè)機(jī)制。等到程序運(yùn)行時(shí)块仆,JVM先初始化构蹬,在JVM初始化的過(guò)程中,JVM生成幾個(gè)ClassLoader悔据,JVM調(diào)用指定的ClassLoader去加載.class文件等各類(lèi)路徑庄敛、文件的類(lèi)。
- 程序運(yùn)行時(shí)類(lèi)的加載實(shí)際過(guò)程
- JDK執(zhí)行指令去尋找jre目錄科汗,尋找jvm.dll藻烤,并初始化JVM;
- 產(chǎn)生一個(gè)Bootstrap Loader(啟動(dòng)類(lèi)加載器)头滔;
- BootstrapLoader自動(dòng)加載ExtendedLoader(標(biāo)準(zhǔn)擴(kuò)展類(lèi)加載器)怖亭,并將其父Loader設(shè)為Bootstrap Loader。
- BootstrapLoader自動(dòng)加載AppClassLoader(系統(tǒng)類(lèi)加載器)坤检,并將其父Loader設(shè)為Extended Loader兴猩。
- 最后由AppClassLoader加載HelloWorld類(lèi)。
- 各種ClassLoader及其特點(diǎn)
- BootstrapLoader(啟動(dòng)類(lèi)加載器):加載System.getProperty("sun.boot.class.path")所指定的路徑或jar
- ExtendedLoader(標(biāo)準(zhǔn)擴(kuò)展類(lèi)加載器ExtClassLoader):加載System.getProperty("java.ext.dirs")所指定的路徑或jar早歇。在使用Java運(yùn)行程序時(shí)倾芝,也可以指定其搜索路徑讨勤,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld
- AppClassLoader(系統(tǒng)類(lèi)加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar。在使用Java運(yùn)行程序時(shí)晨另,也可以加上-cp來(lái)覆蓋原有的Classpath設(shè)置潭千,例如: java -cp ./lavasoft/classes HelloWorld
-
特點(diǎn)
- ExtClassLoader和AppClassLoader在JVM啟動(dòng)后,會(huì)在JVM中保存一份借尿,并且在程序運(yùn)行中無(wú)法改變其搜索路徑刨晴。如果想在運(yùn)行時(shí)從其他搜索路徑加載類(lèi),就要產(chǎn)生新的類(lèi)加載器垛玻。
- 運(yùn)行一個(gè)程序時(shí),總是由AppClassLoader(系統(tǒng)類(lèi)加載器)開(kāi)始加載指定的類(lèi)
- 在加載類(lèi)時(shí)奶躯,每個(gè)類(lèi)加載器會(huì)將加載任務(wù)上交給其父帚桩,如果其父找不到,再由自己去加載
- BootstrapLoader(啟動(dòng)類(lèi)加載器)是最頂級(jí)的類(lèi)加載器了嘹黔,其父加載器為null
-
各類(lèi)ClassLoader的關(guān)系圖解(幫助理解)
注意:圖解中可得账嚎,執(zhí)行代碼
c.getClassLoader().getParent().getParent()
為null
,由于get不到BootstrapLoader儡蔓,因?yàn)锽ootstrapLoader是C層次實(shí)現(xiàn)的郭蕉。
- 關(guān)于不同類(lèi)加載器所處命名空間不同的問(wèn)題理解
Java當(dāng)中,不同的類(lèi)加載器加載的類(lèi)在虛擬機(jī)中位于不同命名空間下喂江,而不同命名空間下的類(lèi)相互不可見(jiàn)召锈。那么,有時(shí)我們會(huì)很不解获询,BootstrapLoader加載java.util.List類(lèi)涨岁,而我們自己定義的類(lèi)比如com.androidjp.MyClass 則由APPClassLoader來(lái)加載。從上面的圖中我們可以看到吉嚣,APPClassLoader等ClassLoader之間是繼承與被繼承的關(guān)系梢薪,而APPClassLoader本身可以作為我們自定義ClassLoader的父類(lèi),當(dāng)默認(rèn)調(diào)用APPClassLoader去加載某個(gè)類(lèi)時(shí)尝哆,它先在它的緩存區(qū)查看要加載的這個(gè)類(lèi)是否存在秉撇,存在則直接加載,不存在則它會(huì)讓它的父類(lèi)ExtendedLoader去加載秋泄,而ExtendedLoader又會(huì)調(diào)用進(jìn)行同樣的步驟琐馆,直到他的父親BootstrapLoader,這種調(diào)用關(guān)系我們稱(chēng)之為“雙親委托機(jī)制”恒序。在這種機(jī)制下啡捶,原本在JVM的BootstrapLoader類(lèi)型表【JVM為每一個(gè)類(lèi)加載器維護(hù)一個(gè)表,表中存放所有以這個(gè)類(lèi)加載器為初始類(lèi)加載器的類(lèi)】中的java.util.List,就能夠被這個(gè)MyClass所發(fā)現(xiàn)奸焙,因?yàn)樗麄儍蓚€(gè)類(lèi)之間相互是‘融洽’的瞎暑,換句話(huà)說(shuō)彤敛,MyClass的初始類(lèi)加載器的先輩所加載的類(lèi),也是我的親人了赌,我們之間的交互是安全的墨榄。所以, 才有了‘MyClass中可以調(diào)用加載List’的過(guò)程勿她。詳細(xì)可以點(diǎn)擊這篇文章來(lái)參考袄秩。
二、類(lèi)的加載方式
- 方式一:命令行啟動(dòng)應(yīng)用時(shí)候由JVM初始化加載
- 方式二:通過(guò)Class.forName()方法動(dòng)態(tài)加載(默認(rèn)會(huì)執(zhí)行初始化塊逢并,但如果指定ClassLoader之剧,初始化時(shí)不執(zhí)行靜態(tài)塊 )
- 方式三:通過(guò)ClassLoader.loadClass()方法動(dòng)態(tài)加載(不會(huì)執(zhí)行初始化塊 )
解析:
方式一其實(shí)就是通過(guò)以下幾種主動(dòng)引用類(lèi)的方式所觸發(fā)的JVM的類(lèi)加載和初始化過(guò)程。然后砍聊,其實(shí)這三種類(lèi)加載方式背稼,在java 層面上都是JVM調(diào)用了ClassLoader去加載類(lèi)的過(guò)程,只是:方式一相對(duì)與方式二和方式三而言玻蝌,屬于靜態(tài)方式的加載蟹肘;而方式二和方式三的區(qū)別,在于Class.ForName
源碼中:
///Class.forname(String name)
public static Class<?> forName(String className) throws ClassNotFoundException {
return forName(className, true, VMStack.getCallingClassLoader());
}
………………
///實(shí)際調(diào)用:
public static Class<?> forName(String className, boolean shouldInitialize,
ClassLoader classLoader) throws ClassNotFoundException {
if (classLoader == null) {
classLoader = BootClassLoader.getInstance();
}
Class<?> result;
try {
result = classForName(className, shouldInitialize, classLoader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof LinkageError) {
throw (LinkageError) cause;
}
throw e;
}
return result;
}
在源碼當(dāng)中可以看到俯树,參數(shù)boolean shouldInitialize
帘腹,在默認(rèn)情況下的Class.forName(String)此參數(shù)默認(rèn)為true
,則默認(rèn)情況下會(huì)進(jìn)行初始化许饿,
那么阳欲,初始化到時(shí)是怎么個(gè)操作過(guò)程,此過(guò)程又是怎么樣去觸發(fā)的呢陋率?下面我們通過(guò)分析類(lèi)的加載流程以及整體圖解廓潜,來(lái)幫助說(shuō)明涕滋。
三、詳細(xì)分析整個(gè)類(lèi)的加載流程
下面分析一下類(lèi)的幾種加載方式、ClassLoader對(duì)類(lèi)加載的背后败晴,是怎么個(gè)原理:
1. 類(lèi)從編譯屑那、被使用路捧,到卸載的全過(guò)程:
<u>編譯 -> 加載 -> 鏈接(驗(yàn)證+準(zhǔn)備+解析)->初始化(使用前的準(zhǔn)備)->使用-> 卸載</u>
2. 類(lèi)的初始化之前
加載(除了自定義加載)和鏈接的過(guò)程是完全由jvm負(fù)責(zé)的箩退,包括:加載 -> 驗(yàn)證 -> 準(zhǔn)備 -> 解析
這里的“自定義加載”可以理解為:自定義類(lèi)加載器去實(shí)現(xiàn)自定義路徑中類(lèi)的加載,可以參考這篇文章芍耘。由于默認(rèn)各個(gè)路徑的類(lèi)文件加載過(guò)程在JVM初始化的過(guò)程中就默認(rèn)設(shè)定好了址遇,也就是一般步驟下的加載過(guò)程,已經(jīng)在JVM初始化過(guò)程中規(guī)定的AppClassLoader等加載器中規(guī)定了步驟斋竞,所以倔约,按一般的加載步驟,就是按JVM規(guī)定的順序坝初,JVM肯定先負(fù)責(zé)了類(lèi)的加載和鏈接處理浸剩,然后再進(jìn)行類(lèi)初始化钾军。
- 首先是加載:
- 此過(guò)程由類(lèi)加載器完成
- 這一塊JVM要完成3件事:
- 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)绢要。
- 在java堆中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象吏恭,作為方法區(qū)這些數(shù)據(jù)的訪(fǎng)問(wèn)入口。
- 這一步很靈活,很多技術(shù)都是在這里切入重罪,因?yàn)樗](méi)有限定二進(jìn)制流從哪里來(lái)樱哼,那么我們可以<u>用系統(tǒng)的類(lèi)加載器,也可以用自己的方式寫(xiě)加載器來(lái)控制字節(jié)流的獲取</u>:
- 從class文件來(lái)->一般的文件加載
- 從zip包中來(lái)->加載jar中的類(lèi)
- 從網(wǎng)絡(luò)中來(lái)->Applet
- 獲取二進(jìn)制流獲取完成后會(huì)按照jvm所需的方式保存在方法區(qū)中剿配,同時(shí)會(huì)在java堆中實(shí)例化一個(gè)java.lang.Class對(duì)象與堆中的數(shù)據(jù)關(guān)聯(lián)起來(lái)搅幅。
- 然后是驗(yàn)證(也稱(chēng)為檢驗(yàn)):
一句話(huà):檢查代碼的:完整性、正確性呼胚、安全性
-
主要經(jīng)歷幾個(gè)步驟:文件格式驗(yàn)證->元數(shù)據(jù)驗(yàn)證->字節(jié)碼驗(yàn)證->符號(hào)引用驗(yàn)證
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范并 驗(yàn)證其版本是否能被當(dāng)前的jvm版本所處理茄唐。ok沒(méi)問(wèn)題后,字節(jié)流就可以進(jìn)入內(nèi)存的方法區(qū)進(jìn)行保存了砸讳。后面的3個(gè)校驗(yàn)都是在方法區(qū)進(jìn)行的琢融。
- 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義化分析界牡,保證其描述的內(nèi)容符合java語(yǔ)言的語(yǔ)法規(guī)范簿寂。
- 字節(jié)碼檢驗(yàn):最復(fù)雜,對(duì)方法體的內(nèi)容進(jìn)行檢驗(yàn)宿亡,保證其在運(yùn)行時(shí)不會(huì)作出什么出格的事來(lái)常遂。
- 符號(hào)引用驗(yàn)證:來(lái)驗(yàn)證一些引用的真實(shí)性與可行性,比如代碼里面引了其他類(lèi)挽荠,這里就要去檢測(cè)一下那些來(lái)究竟是否存在克胳;或者說(shuō)代碼中訪(fǎng)問(wèn)了其他類(lèi)的一些屬性,這里就對(duì)那些屬性的可以訪(fǎng)問(wèn)行進(jìn)行了檢驗(yàn)圈匆。(這一步將為后面的解析工作打下基礎(chǔ))
- 目的:確保class文件的字節(jié)流信息符合jvm的口味漠另,不會(huì)讓jvm感到不舒服。假如class文件是由純粹的java代碼編譯過(guò)來(lái)的跃赚,自然不會(huì)出現(xiàn)類(lèi)似于數(shù)組越界笆搓、跳轉(zhuǎn)到不存在的代碼塊等不健康的問(wèn)題,因?yàn)橐坏┏霈F(xiàn)這種現(xiàn)象纬傲,編譯器就會(huì)拒絕編譯了满败。但是,跟之前說(shuō)的一樣叹括,Class文件流不一定是從java源碼編譯過(guò)來(lái)的算墨,也可能是從網(wǎng)絡(luò)或者其他地方過(guò)來(lái)的,甚至你可以自己用16進(jìn)制寫(xiě)汁雷,假如jvm不對(duì)這些數(shù)據(jù)進(jìn)行校驗(yàn)的話(huà)净嘀,可能一些有害的字節(jié)流會(huì)讓jvm完全崩潰报咳。
驗(yàn)證階段很重要,但也不是必要的面粮,假如說(shuō)一些代碼被反復(fù)使用并驗(yàn)證過(guò)可靠性了少孝,實(shí)施階段就可以嘗試用-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類(lèi)驗(yàn)證措施,以簡(jiǎn)短類(lèi)加載時(shí)間熬苍。
- 隨后是準(zhǔn)備:
一句話(huà):為靜態(tài)域分配存儲(chǔ)空間
- 這階段會(huì)為類(lèi)變量(指那些靜態(tài)變量)分配內(nèi)存并設(shè)置類(lèi)比那輛初始值的階段稍走,這些內(nèi)存在方法區(qū)中進(jìn)行分配。這里要說(shuō)明一下柴底,這一步只會(huì)給那些靜態(tài)變量設(shè)置一個(gè)初始的值婿脸,而那些實(shí)例變量是在實(shí)例化對(duì)象時(shí)進(jìn)行分配的。
例如:-
public static int value=123;
此時(shí)value的值為0柄驻,不是123狐树。 -
private int i = 123;
此時(shí),i 還未進(jìn)行初始化鸿脓,因?yàn)檫@句代碼還不能執(zhí)行抑钟。
-
- 最后是解析:
一句話(huà):符號(hào)引用 -> 直接引用
- 是對(duì)類(lèi)的字段,方法等東西進(jìn)行轉(zhuǎn)換野哭,具體涉及到Class文件的格式內(nèi)容在塔。
3. 類(lèi)的初始化條件(主動(dòng)對(duì)類(lèi)進(jìn)行引用)
說(shuō)明:要對(duì)類(lèi)進(jìn)行初始化,代碼上可以理解為<u>‘為要初始化的類(lèi)中的所有靜態(tài)成員都賦予初始值拨黔、對(duì)類(lèi)中所有靜態(tài)塊都執(zhí)行一次蛔溃,并且是按代碼編寫(xiě)順序執(zhí)行’</u>。
如下代碼:輸出的是‘1’篱蝇。如果①和②順序調(diào)換贺待,則輸出的是‘123’。
public class Main {
public static void main(String[] args){
System.out.println(Super.i);
}
}
class Super{
//①
static{
i = 123;
}
//②
protected static int i = 1;
}
- 遇到new零截,getstatic麸塞,putstatic,invokestatic這4條字節(jié)碼指令時(shí)涧衙,假如類(lèi)還沒(méi)進(jìn)行初始化哪工,則馬上對(duì)其進(jìn)行初始化工作。
其實(shí)就是3種情況:
- 用new實(shí)例化一個(gè)類(lèi)時(shí)
- 讀取或者設(shè)置類(lèi)的靜態(tài)字段時(shí)(不包括被final修飾的靜態(tài)字段绍撞,因?yàn)樗麄円呀?jīng)被塞進(jìn)常量池了)
- 執(zhí)行靜態(tài)方法的時(shí)候正勒。
- 使用java.lang.reflect.*的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)還沒(méi)有進(jìn)行過(guò)初始化傻铣,馬上對(duì)其進(jìn)行章贞。
- 初始化一個(gè)類(lèi)的時(shí)候,如果他的父親還沒(méi)有被初始化,則先去初始化其父親鸭限。
- 當(dāng)jvm啟動(dòng)時(shí)蜕径,用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含static void main(String[] args)的那個(gè)類(lèi)),則jvm會(huì)先去初始化這個(gè)類(lèi)败京。
- 用Class.forName(String className);來(lái)加載類(lèi)的時(shí)候兜喻,也會(huì)執(zhí)行初始化動(dòng)作。
【注意:ClassLoader的loadClass(String className);方法只會(huì)加載并編譯某類(lèi)赡麦,并不會(huì)對(duì)其執(zhí)行初始化】
說(shuō)明:“主動(dòng)對(duì)類(lèi)進(jìn)行引用”指的就是以上五種JVM規(guī)定的判定初始化與否的預(yù)處理?xiàng)l件朴皆。
那么,其他的方式泛粹,都可歸為‘類(lèi)被動(dòng)引用’的方式遂铡,這些方式是不會(huì)引起JVM去初始化相關(guān)類(lèi)的:
- <u>子類(lèi)調(diào)用父類(lèi)</u>的靜態(tài)變量(子類(lèi)不會(huì)進(jìn)行初始化,父類(lèi)會(huì)初始化)
- 通過(guò)<u>數(shù)組</u>引用類(lèi)的情況(類(lèi)Main不會(huì)被初始化)
如:list = Main[10]; - 調(diào)用類(lèi)中的<u>final靜態(tài)常量</u>(類(lèi)不會(huì)被初始化)
四晶姊、原理分析圖解
類(lèi)加載中每個(gè)部分詳細(xì)的原理說(shuō)明扒接,可以查看這篇文章。以下的圖解為本人總結(jié)们衙,算比較全地對(duì)每個(gè)步驟的原理過(guò)程一目了然:
說(shuō)明: 圖解左下角說(shuō)的
<clinit>()
方法钾怔,概念上是一個(gè)方法塊,這個(gè)<clinit>(){……}方法塊在初始化過(guò)程中執(zhí)行蒙挑,可以用下面代碼理解:
class Parent{
public static int A=1;
static{
A=2;
}
}
---相當(dāng)于---->
class Parent{
<clinit>(){
public static int A=1;
static{
A=2;
}
}
}
相當(dāng)于把靜態(tài)變量的賦值和靜態(tài)代碼塊等操作順序串連成一個(gè)方法宗侦。
注意:
- 對(duì)于類(lèi),會(huì)生成<clinit>(){……}方法體:去包含靜態(tài)變量的賦值和靜態(tài)塊代碼
- 而對(duì)于接口脆荷,也會(huì)生成<clinit>(){……}方法體:去初始化接口中的成員變量
- 接口和類(lèi)初始化過(guò)程的區(qū)別:類(lèi)的初始化執(zhí)行之前要求父類(lèi)全部都初始化完成了凝垛,但接口的初始化貌似對(duì)父接口的初始化不怎么感冒懊悯,也就是說(shuō)蜓谋,子接口初始化的時(shí)候并不要求其父接口也完成初始化,只有在真正使用到父接口的時(shí)候它才會(huì)被初始化(比如引用接口上的常量的時(shí)候啦)
五炭分、簡(jiǎn)單代碼示例說(shuō)明
這里桃焕,用一個(gè)java代碼示例,來(lái)根據(jù)輸出得到的各個(gè)方法和塊的執(zhí)行順序捧毛,去更加形象地理解整個(gè)類(lèi)的加載和運(yùn)行過(guò)程:
public class Main {
public static void main(String[] args){
System.out.println("我是main方法观堂,我輸出Super的類(lèi)變量i:"+Sub.i);
Sub sub = new Sub();
}
}
class Super{
{
System.out.println("我是Super成員塊");
}
public Super(){
System.out.println("我是Super構(gòu)造方法");
}
{
int j = 123;
System.out.println("我是Super成員塊中的變量j:"+j);
}
static{
System.out.println("我是Super靜態(tài)塊");
i = 123;
}
protected static int i = 1;
}
class Sub extends Super{
static{
System.out.println("我是Sub靜態(tài)塊");
}
public Sub(){
System.out.println("我是Sub構(gòu)造方法");
}
{
System.out.println("我是Sub成員塊");
}
}
得到結(jié)果為:
說(shuō)明:
- 對(duì)于同一個(gè)類(lèi):靜態(tài)代碼塊和靜態(tài)變量的賦值 是先于main方法的調(diào)用執(zhí)行的。
- 對(duì)于同一個(gè)類(lèi):靜態(tài)代碼塊和靜態(tài)變量的賦值是按順序執(zhí)行的呀忧。
- 子類(lèi)調(diào)用父類(lèi)的類(lèi)變量成員师痕,是不會(huì)觸發(fā)子類(lèi)本身的初始化操作的【所以我們調(diào)用
Sub.i
,Sub.class
并沒(méi)有被初始化和加載】而账。 - 使用new方式創(chuàng)建子類(lèi)胰坟,對(duì)于類(lèi)加載而言,是先加載父類(lèi)泞辐、再加載子類(lèi)(注意:此時(shí)由于父類(lèi)已經(jīng)在前面初始化了一次笔横,所以竞滓,這一步,就只有子類(lèi)初始化吹缔,父類(lèi)不會(huì)再進(jìn)行初始化)
- 不論成員塊放在哪個(gè)位置商佑,它都 先于 類(lèi)構(gòu)造方法執(zhí)行。
參考文章
http://my.oschina.net/volador/blog/87194
http://lavasoft.blog.51cto.com/62575/184547/