轉(zhuǎn)載請說明出處:Java面試相關(一)-- Java類加載全過程
JVM判斷并裝載類的過程
概述:
我們知道,Java中我們寫類的代碼,是存在于一個個.java文件中的,而這個后綴名也是讓JVM識別編譯的基礎。可能有些Android開發(fā)者對幾個ClassLoader(如:AppClassLoader等)比較熟悉,那么,整個類的加載過程:從未進行編譯的.java文件,到類的初始化完畢并等待被實例化使用的過程,具體是怎么樣的。
??收集Java資料、看了幾篇本人覺得較好的博文后,總結(jié)以下關于Java類的加載過程,掌握此過程,能夠更加理解Java類的各個方法的執(zhí)行順序,以及JVM的工作和Java類生成的原理。讀者笑納~
類的加載過程分析
類從.java文件到實際加載到內(nèi)存中,實際上是這樣的一個過程:
??.java文件 -> 通過你的JDK環(huán)境相關指令編譯 -> .class文件 -> JVM初始化之后,如果有類的執(zhí)行、調(diào)用等相關操作,JVM就會將.class文件加載到內(nèi)存中,并開始下面的一系列處理:(鏈接->初始化)
一、關于ClassLoader
首先我們要搞清楚一點,ClassLoader是Java用于加載類的一個機制。等到程序運行時,JVM先初始化,在JVM初始化的過程中,JVM生成幾個ClassLoader,JVM調(diào)用指定的ClassLoader去加載.class文件等各類路徑、文件的類。
- 程序運行時類的加載實際過程
- JDK執(zhí)行指令去尋找jre目錄,尋找jvm.dll,并初始化JVM;
- 產(chǎn)生一個Bootstrap Loader(啟動類加載器);
- BootstrapLoader自動加載ExtendedLoader(標準擴展類加載器),并將其父Loader設為Bootstrap Loader。
- BootstrapLoader自動加載AppClassLoader(系統(tǒng)類加載器),并將其父Loader設為Extended Loader。
- 最后由AppClassLoader加載HelloWorld類。
- 各種ClassLoader及其特點
- BootstrapLoader(啟動類加載器):加載System.getProperty("sun.boot.class.path")所指定的路徑或jar
- ExtendedLoader(標準擴展類加載器ExtClassLoader):加載System.getProperty("java.ext.dirs")所指定的路徑或jar。在使用Java運行程序時,也可以指定其搜索路徑,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld
- AppClassLoader(系統(tǒng)類加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar。在使用Java運行程序時,也可以加上-cp來覆蓋原有的Classpath設置,例如: java -cp ./lavasoft/classes HelloWorld
-
特點
- ExtClassLoader和AppClassLoader在JVM啟動后,會在JVM中保存一份,并且在程序運行中無法改變其搜索路徑。如果想在運行時從其他搜索路徑加載類,就要產(chǎn)生新的類加載器。
- 運行一個程序時,總是由AppClassLoader(系統(tǒng)類加載器)開始加載指定的類
- 在加載類時,每個類加載器會將加載任務上交給其父,如果其父找不到,再由自己去加載
- BootstrapLoader(啟動類加載器)是最頂級的類加載器了,其父加載器為null
-
各類ClassLoader的關系圖解(幫助理解)
各個ClassLoader的作用以及他們之間的關系.png
注意:圖解中可得,執(zhí)行代碼
c.getClassLoader().getParent().getParent()為null,由于get不到BootstrapLoader,因為BootstrapLoader是C層次實現(xiàn)的。
- 關于不同類加載器所處命名空間不同的問題理解
Java當中,不同的類加載器加載的類在虛擬機中位于不同命名空間下,而不同命名空間下的類相互不可見。那么,有時我們會很不解,BootstrapLoader加載java.util.List類,而我們自己定義的類比如com.androidjp.MyClass 則由APPClassLoader來加載。從上面的圖中我們可以看到,APPClassLoader等ClassLoader之間是繼承與被繼承的關系,而APPClassLoader本身可以作為我們自定義ClassLoader的父類,當默認調(diào)用APPClassLoader去加載某個類時,它先在它的緩存區(qū)查看要加載的這個類是否存在,存在則直接加載,不存在則它會讓它的父類ExtendedLoader去加載,而ExtendedLoader又會調(diào)用進行同樣的步驟,直到他的父親BootstrapLoader,這種調(diào)用關系我們稱之為“雙親委托機制”。在這種機制下,原本在JVM的BootstrapLoader類型表【JVM為每一個類加載器維護一個表,表中存放所有以這個類加載器為初始類加載器的類】中的java.util.List,就能夠被這個MyClass所發(fā)現(xiàn),因為他們兩個類之間相互是‘融洽’的,換句話說,MyClass的初始類加載器的先輩所加載的類,也是我的親人,我們之間的交互是安全的。所以, 才有了‘MyClass中可以調(diào)用加載List’的過程。詳細可以點擊這篇文章來參考。
二、類的加載方式
- 方式一:命令行啟動應用時候由JVM初始化加載
- 方式二:通過Class.forName()方法動態(tài)加載(默認會執(zhí)行初始化塊,但如果指定ClassLoader,初始化時不執(zhí)行靜態(tài)塊 )
- 方式三:通過ClassLoader.loadClass()方法動態(tài)加載(不會執(zhí)行初始化塊 )
解析:
方式一其實就是通過以下幾種主動引用類的方式所觸發(fā)的JVM的類加載和初始化過程。然后,其實這三種類加載方式,在java 層面上都是JVM調(diào)用了ClassLoader去加載類的過程,只是:方式一相對與方式二和方式三而言,屬于靜態(tài)方式的加載;而方式二和方式三的區(qū)別,在于Class.ForName源碼中:
///Class.forname(String name)
public static Class<?> forName(String className) throws ClassNotFoundException {
return forName(className, true, VMStack.getCallingClassLoader());
}
………………
///實際調(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;
}
在源碼當中可以看到,參數(shù)boolean shouldInitialize,在默認情況下的Class.forName(String)此參數(shù)默認為true,則默認情況下會進行初始化,
那么,初始化到時是怎么個操作過程,此過程又是怎么樣去觸發(fā)的呢?下面我們通過分析類的加載流程以及整體圖解,來幫助說明。
三、詳細分析整個類的加載流程
下面分析一下類的幾種加載方式、ClassLoader對類加載的背后,是怎么個原理:
1. 類從編譯、被使用,到卸載的全過程:
<u>編譯 -> 加載 -> 鏈接(驗證+準備+解析)->初始化(使用前的準備)->使用-> 卸載</u>
2. 類的初始化之前
加載(除了自定義加載)和鏈接的過程是完全由jvm負責的,包括:加載 -> 驗證 -> 準備 -> 解析
這里的“自定義加載”可以理解為:自定義類加載器去實現(xiàn)自定義路徑中類的加載,可以參考這篇文章。由于默認各個路徑的類文件加載過程在JVM初始化的過程中就默認設定好了,也就是一般步驟下的加載過程,已經(jīng)在JVM初始化過程中規(guī)定的AppClassLoader等加載器中規(guī)定了步驟,所以,按一般的加載步驟,就是按JVM規(guī)定的順序,JVM肯定先負責了類的加載和鏈接處理,然后再進行類初始化。
- 首先是加載:
- 此過程由類加載器完成
- 這一塊JVM要完成3件事:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
- 在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。
- 這一步很靈活,很多技術都是在這里切入,因為它并沒有限定二進制流從哪里來,那么我們可以<u>用系統(tǒng)的類加載器,也可以用自己的方式寫加載器來控制字節(jié)流的獲取</u>:
- 從class文件來->一般的文件加載
- 從zip包中來->加載jar中的類
- 從網(wǎng)絡中來->Applet
- 獲取二進制流獲取完成后會按照jvm所需的方式保存在方法區(qū)中,同時會在java堆中實例化一個java.lang.Class對象與堆中的數(shù)據(jù)關聯(lián)起來。
- 然后是驗證(也稱為檢驗):
一句話:檢查代碼的:完整性、正確性、安全性
-
主要經(jīng)歷幾個步驟:文件格式驗證->元數(shù)據(jù)驗證->字節(jié)碼驗證->符號引用驗證
- 文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范并 驗證其版本是否能被當前的jvm版本所處理。ok沒問題后,字節(jié)流就可以進入內(nèi)存的方法區(qū)進行保存了。后面的3個校驗都是在方法區(qū)進行的。
- 元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義化分析,保證其描述的內(nèi)容符合java語言的語法規(guī)范。
- 字節(jié)碼檢驗:最復雜,對方法體的內(nèi)容進行檢驗,保證其在運行時不會作出什么出格的事來。
- 符號引用驗證:來驗證一些引用的真實性與可行性,比如代碼里面引了其他類,這里就要去檢測一下那些來究竟是否存在;或者說代碼中訪問了其他類的一些屬性,這里就對那些屬性的可以訪問行進行了檢驗。(這一步將為后面的解析工作打下基礎)
- 目的:確保class文件的字節(jié)流信息符合jvm的口味,不會讓jvm感到不舒服。假如class文件是由純粹的java代碼編譯過來的,自然不會出現(xiàn)類似于數(shù)組越界、跳轉(zhuǎn)到不存在的代碼塊等不健康的問題,因為一旦出現(xiàn)這種現(xiàn)象,編譯器就會拒絕編譯了。但是,跟之前說的一樣,Class文件流不一定是從java源碼編譯過來的,也可能是從網(wǎng)絡或者其他地方過來的,甚至你可以自己用16進制寫,假如jvm不對這些數(shù)據(jù)進行校驗的話,可能一些有害的字節(jié)流會讓jvm完全崩潰。
驗證階段很重要,但也不是必要的,假如說一些代碼被反復使用并驗證過可靠性了,實施階段就可以嘗試用-Xverify:none參數(shù)來關閉大部分的類驗證措施,以簡短類加載時間。
- 隨后是準備:
一句話:為靜態(tài)域分配存儲空間
- 這階段會為類變量(指那些靜態(tài)變量)分配內(nèi)存并設置類比那輛初始值的階段,這些內(nèi)存在方法區(qū)中進行分配。這里要說明一下,這一步只會給那些靜態(tài)變量設置一個初始的值,而那些實例變量是在實例化對象時進行分配的。
例如:-
public static int value=123;此時value的值為0,不是123。 -
private int i = 123;此時,i 還未進行初始化,因為這句代碼還不能執(zhí)行。
-
- 最后是解析:
一句話:符號引用 -> 直接引用
- 是對類的字段,方法等東西進行轉(zhuǎn)換,具體涉及到Class文件的格式內(nèi)容。
3. 類的初始化條件(主動對類進行引用)
說明:要對類進行初始化,代碼上可以理解為<u>‘為要初始化的類中的所有靜態(tài)成員都賦予初始值、對類中所有靜態(tài)塊都執(zhí)行一次,并且是按代碼編寫順序執(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é)碼指令時,假如類還沒進行初始化,則馬上對其進行初始化工作。
其實就是3種情況:
- 用new實例化一個類時
- 讀取或者設置類的靜態(tài)字段時(不包括被final修飾的靜態(tài)字段,因為他們已經(jīng)被塞進常量池了)
- 執(zhí)行靜態(tài)方法的時候。
- 使用java.lang.reflect.*的方法對類進行反射調(diào)用的時候,如果類還沒有進行過初始化,馬上對其進行。
- 初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。
- 當jvm啟動時,用戶需要指定一個要執(zhí)行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。
- 用Class.forName(String className);來加載類的時候,也會執(zhí)行初始化動作。
【注意:ClassLoader的loadClass(String className);方法只會加載并編譯某類,并不會對其執(zhí)行初始化】
說明:“主動對類進行引用”指的就是以上五種JVM規(guī)定的判定初始化與否的預處理條件。
那么,其他的方式,都可歸為‘類被動引用’的方式,這些方式是不會引起JVM去初始化相關類的:
- <u>子類調(diào)用父類</u>的靜態(tài)變量(子類不會進行初始化,父類會初始化)
- 通過<u>數(shù)組</u>引用類的情況(類Main不會被初始化)
如:list = Main[10]; - 調(diào)用類中的<u>final靜態(tài)常量</u>(類不會被初始化)
四、原理分析圖解
類加載中每個部分詳細的原理說明,可以查看這篇文章。以下的圖解為本人總結(jié),算比較全地對每個步驟的原理過程一目了然:

說明: 圖解左下角說的
<clinit>()方法,概念上是一個方法塊,這個<clinit>(){……}方法塊在初始化過程中執(zhí)行,可以用下面代碼理解:
class Parent{
public static int A=1;
static{
A=2;
}
}
---相當于---->
class Parent{
<clinit>(){
public static int A=1;
static{
A=2;
}
}
}
相當于把靜態(tài)變量的賦值和靜態(tài)代碼塊等操作順序串連成一個方法。
注意:
- 對于類,會生成<clinit>(){……}方法體:去包含靜態(tài)變量的賦值和靜態(tài)塊代碼
- 而對于接口,也會生成<clinit>(){……}方法體:去初始化接口中的成員變量
- 接口和類初始化過程的區(qū)別:類的初始化執(zhí)行之前要求父類全部都初始化完成了,但接口的初始化貌似對父接口的初始化不怎么感冒,也就是說,子接口初始化的時候并不要求其父接口也完成初始化,只有在真正使用到父接口的時候它才會被初始化(比如引用接口上的常量的時候啦)
五、簡單代碼示例說明
這里,用一個java代碼示例,來根據(jù)輸出得到的各個方法和塊的執(zhí)行順序,去更加形象地理解整個類的加載和運行過程:
public class Main {
public static void main(String[] args){
System.out.println("我是main方法,我輸出Super的類變量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é)果為:

說明:
- 對于同一個類:靜態(tài)代碼塊和靜態(tài)變量的賦值 是先于main方法的調(diào)用執(zhí)行的。
- 對于同一個類:靜態(tài)代碼塊和靜態(tài)變量的賦值是按順序執(zhí)行的。
- 子類調(diào)用父類的類變量成員,是不會觸發(fā)子類本身的初始化操作的【所以我們調(diào)用
Sub.i,Sub.class并沒有被初始化和加載】。 - 使用new方式創(chuàng)建子類,對于類加載而言,是先加載父類、再加載子類(注意:此時由于父類已經(jīng)在前面初始化了一次,所以,這一步,就只有子類初始化,父類不會再進行初始化)
- 不論成員塊放在哪個位置,它都 先于 類構(gòu)造方法執(zhí)行。
參考文章
http://my.oschina.net/volador/blog/87194
http://lavasoft.blog.51cto.com/62575/184547/

