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