前言
什么是類加載?
虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存嗜憔,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化砍的,最終形成可以被虛擬機直接使用的java類型痹筛。
加載什么?
前面的定義已經(jīng)講了是加載描述類的數(shù)據(jù)廓鞠,也就是Class文件帚稠,關(guān)于Class文件,我在《深入解析Class類文件的結(jié)構(gòu)》一文中進行了分析床佳。
誰來加載滋早?
加載描述類的類文件的二進制流是由類加載器完成的,已有的三種類加載和自定義的類加載器組成了類加載器子系統(tǒng)砌们,關(guān)于類加載器杆麸,下文會詳細講述。
怎么加載浪感?
這就是本文的重點昔头,類加載機制中的類加載流程。
可以通過下圖整體上看一下類加載在JVM體系中的位置
類的生命周期
類的生命周期共有7個階段影兽,分別如下圖:
前5個階段屬于類加載流程的范圍揭斧,其中驗證、準備峻堰、解析又被稱為連接讹开,類加載的5個階段并不是按照順序依次完成的盅视,除了解析可能會在初始化之后開始,其他的幾個階段的開始順序是確定的旦万,但結(jié)束順序不一定闹击,可能會交叉著進行,加載還沒完成成艘,連接可能已經(jīng)開始赏半。
類加載流程
類加載分為5個過程,分別是加載狰腌、驗證除破、準備、解析琼腔、初始化瑰枫,下面分別對這幾個過程進行講述,盡量簡短明了丹莲。
加載
"加載"是"類加載"流程的一個階段
加載階段主要干的3件事:
- 通過一個類的全限定名獲取定義此類的二進制字節(jié)流
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的java.lang.Class實例光坝,作為訪問入口
在這三件事里,開發(fā)人員能干預的是第一件事甥材,我們可以使用系統(tǒng)的三個類加載器去加載我們想要加載的類文件盯另,也可以自定義類加載器去獲取二進制字節(jié)流。
定義類的二進制字節(jié)流不一定是經(jīng)過編譯后存儲在磁盤上的.class文件洲赵,有可能是以下來源:
- 從ZIP包中讀取鸳惯,如:JAR、EAR叠萍、WAR
- 從網(wǎng)絡中獲取芝发,如:Applet
- 運行時計算生成,如:動態(tài)代理技術(shù)
- 由其他文件生成苛谷,如:JSP文件生成.class
- 從數(shù)據(jù)庫中讀取辅鲸,中間件服務器,如:SAP Netweaver
Hotspot虛擬機中腹殿,Class實例不是在堆上分配空間独悴,而是存放在方法區(qū)中,這個實例在代碼中可以輕松的獲取到锣尉,并通過它可以獲取代表某個類的各種數(shù)據(jù)結(jié)構(gòu)刻炒。
驗證
驗證是對輸入的字節(jié)流進行檢查的過程
為什么要有驗證這個過程呢?就是因為加載的對象:描述類的二進制字節(jié)流自沧,來源廣泛落蝙,不得不防止它被小人利用,損害虛擬機的正常運行,導致崩潰筏勒。所以總共有四個驗證過程,分別如下圖:
- 文件格式驗證
這個階段直接操作字節(jié)流旺嬉,后面的三個階段是基于方法區(qū)的存儲結(jié)構(gòu)管行,這個階段主要是驗證文件本身的字節(jié)碼是不是符合規(guī)范,目的是保證輸入的字節(jié)流可以被正確的存儲在方法區(qū)內(nèi)邪媳。上圖中的四個檢查項只是其中的一小部分捐顷,真正的驗證點還有很多。 - 元數(shù)據(jù)驗證
這個階段主要是驗證類的元數(shù)據(jù)信息是否符合Java語言規(guī)范雨效,比如檢查是否有父類迅涮,除了Objec,其他類都應該要有父類徽龟,否則就不符合規(guī)范了叮姑;被final修飾的不允許被繼承。 - 字節(jié)碼驗證
這個階段主要是對類的方法體進行驗證据悔,保證類方法的運行不會對虛擬機造成危害传透。這是4個驗證里最復雜的一個,因為要通過數(shù)據(jù)流和控制流的分析极颓,確定程序語義是合法的朱盐、符合邏輯的。 - 符號引用驗證
上面三個階段是對類本身進行驗證菠隆,而符號引用驗證階段主要是對類以外的信息進行驗證兵琳,后面會講到解析是將符號引用替換成直接引用,所以這里驗證的目的是確保符號引用是正確的骇径,確保后面的解析過程能順利的進行躯肌。
準備
準備階段是正式為類變量分配內(nèi)存并設置類變量初始值的階段
注意這里是為類變量分配內(nèi)存,而且是分配在方法區(qū)中既峡,實例變量是后面隨著實例一起分配在堆上的羡榴。
設置初始值也不是代碼里賦的值,而是各個數(shù)據(jù)類型規(guī)定的零值运敢,比如基礎類型是相應類型不同字節(jié)長度的0校仑,引用類型是null。
不是每個類變量都是設置為零值传惠,被final修飾的常量迄沫,因為在編譯期帶有一個ConstantValue屬性,屬性值則是該常量在代碼里賦的值卦方,這個值在準備階段前就已經(jīng)確定了羊瘩,所以在準備階段設置值的時候,直接取的ConstantValue給類常量。
下面的例子可以很好的了解準備階段尘吗,準備階段過后逝她,a、b睬捶、c分別是多少黔宛?
public class Test {
public static int a;
public static int b = 1;
public static final int c = 2;
public void say(){
System.out.println("Hello");
}
}
答案揭曉:0, 0, 2 原因上文里寫的很明白
解析
解析是將常量池內(nèi)的符號引用替換為直接引用的過程
那什么是符號引用和直接引用呢?
- 符號引用:以一組符號來描述所引用的目標擒贸,符號可以是任何形式的字面量臀晃,只要使用時能無歧義的定位到目標即可。
- 直接引用:直接引用可以是直接指向目標的指針介劫、相對偏移量或是一個能間接定位到目標的句柄徽惋。
符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標并不一定已經(jīng)加載到內(nèi)存中座韵。各種虛擬機實現(xiàn)的內(nèi)存布局可以各不相同险绘,但是他們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規(guī)范的Class文件格式中回右。
直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的隆圆,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用翔烁,那引用的目標必定已經(jīng)在內(nèi)存中存在渺氧。
解析的時機根據(jù)虛擬機實現(xiàn)不同而不同,可以是類加載器加載時解析蹬屹,也可以是符號引用使用前解析
解析主要是對7類符號引用進行:類或接口侣背、字段、類方法慨默、接口方法贩耐、方法類型、方法句柄厦取、調(diào)用點限定符
初始化
初始化是執(zhí)行類構(gòu)造器
<clinit>()
方法的過程
類初始化階段是類加載流程的最后一個階段潮太,是執(zhí)行<clinit>()
方法的階段,這個階段才真正開始執(zhí)行開發(fā)人員的代碼虾攻。
<clinit>()
方法是編譯器按照源文件中定義的順序收集類變量和靜態(tài)語句塊形成的方法铡买。它的一些特點和細節(jié)如下:
- 編譯器自動收集靜態(tài)變量和靜態(tài)代碼塊合并產(chǎn)生的
- 不需要顯示的調(diào)用父類的
<clinit>
,虛擬機保證父類先執(zhí)行 - 父類定義的靜態(tài)語句塊優(yōu)先于子類變量賦值操作
- 沒有靜態(tài)變量和靜態(tài)語句塊霎箍,可以不生成
<clinit>()
方法 - 接口也會有這個方法奇钞,但不需要先執(zhí)行父類的
<clinit>()
方法 - 虛擬機保證該方法在多線程環(huán)境下被正確的加鎖和同步
什么時候發(fā)生初始化?
對一個類進行主動引用的時候必須初始化漂坏,主動引用的場景如下:
- 遇到new景埃、getstatic媒至、putstatic、invokestatic這四條指令時
- 使用java.lang.reflect包的方法對類進行反射調(diào)用時
- 初始化一個其父類還沒被初始化的類時
- 虛擬機啟動時谷徙,包含main方法的主類還沒被初始化時
- 當使用動態(tài)語言支持時拒啰,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic蒂胞、REF_invokeStatic的方法句柄图呢,并且這個方法所對應的類沒有進行初始化時
什么時候不發(fā)生初始化?
對一個類進行被動引用的時候不初始化骗随,被動引用的場景有下面一些:
- 通過子類引用父類的靜態(tài)字段,不會導致子類的初始化
- 通過數(shù)組定義來引用類赴叹,不會觸發(fā)此類的初始化
- 引用類的常量時鸿染,不會觸發(fā)此類的初始化
類加載器
什么是類加載器?
實現(xiàn)“通過一個類的全限定名來獲取描述此類的二進制字節(jié)流”這個動作的代碼模塊就叫做類加載器
類加載不僅僅是加載二進制字節(jié)碼的作用乞巧,還起著獨立的類名稱空間的作用涨椒,確定一個類的唯一性由三個因素決定:
- 同一個java虛擬機
- 同一個類加載器
- 同一個全限定類名
雙親委派模型
下圖中各個加載器之間的層次關(guān)系被稱為類加載器的雙親委派模型
圖中可以看到,系統(tǒng)提供了三個類加載器:啟動類加載器绽媒、擴展類加載器和應用程序類加載器蚕冬,java程序啟動的時候,三個類加載器分別從各自指定的路徑中加載所需的類是辕。最下面是開發(fā)人員自定義的類加載器囤热,繼承自ClassLoader,重寫findClass()方法获三。
一般我們自己寫的類是默認由應用程程序加載器加載的旁蔼,自定義的類加載器的父類加載器默認是應用程序加載器,應用程序加載器的父類加載器是擴展類加載器疙教,擴展類加載器的父類加載器是啟動類加載器棺聊,這種父子關(guān)系不是一般的繼承或?qū)崿F(xiàn)關(guān)系,而是子加載器持有父加載器的引用贞谓,是一種組合關(guān)系限佩。自定義類加載器時,可以在構(gòu)造函數(shù)中傳入指定的父類加載器裸弦。
雙親委派模型的工作原理
一個類加載器收到了類加載的請求時祟同,它首先會先檢查自身有沒有加載過這個類,實質(zhì)就是在JVM的常量池中查找該類的符號引用是否存在烁兰,如果有就直接返回耐亏,否則把這個請求委派給父類加載器,直至委派給啟動類加載器沪斟,只有當父類加載器加載失敗广辰,子類加載器才會嘗試自己去加載暇矫。
下面是實現(xiàn)雙親委派模型的主要代碼,代碼簡單易懂:
//ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加鎖择吊,整個類加載期間都持有鎖
synchronized (getClassLoadingLock(name)) {
// 首先李根,檢查此類是否已被加載過,是的話直接返回
Class<?> c = findLoadedClass(name);
if (c == null) { //如果沒有加載過几睛,則繼續(xù)
long t0 = System.nanoTime();
try {
if (parent != null) { //有父類加載器房轿,則交給父類加載器加載,遞歸執(zhí)行l(wèi)oadClass方法
c = parent.loadClass(name, false);
} else { //沒有父類加載器,交給啟動類加載器加載所森,執(zhí)行一個本地方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 除了啟動類加載器之外的類加載器加載類失敗拋異常囱持,此處不進行任何處理
}
if (c == null) {
// 父類加載器未成功加載到類,則調(diào)用本加載器的findClass方法
long t1 = System.nanoTime();
c = findClass(name);
// 記錄一些狀態(tài)
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//驗證解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
雖然易懂焕济,但配合下面的圖更容易加深理解纷妆,下面是這段代碼的數(shù)據(jù)流程圖:
下面按照一般的雙親委派模型來分析,假設是自定義的類加載器調(diào)用了loadClass方法晴弃,觸發(fā)了類加載的過程掩幢,則下面的過程會依次執(zhí)行:
- 自定義的類加載器首先會調(diào)用findLoadedClass(name)方法查看有沒有被加載的這個類,如果有直接返回上鞠,否則執(zhí)行下面步驟
- 檢查是否存在父類际邻,如果有則遞歸調(diào)用父類的loadClass方法,否則說明父類加載器是啟動類加載器芍阎,本類加載器是擴展類加載器世曾,調(diào)用findBootstrapClassOrNull(name)使用啟動類加載器進行類加載
- 啟動類加載器加載成功則返回,失敗則調(diào)用擴展類加載器的findClass(name)方法來加載能曾,成功則返回度硝,失敗則繼續(xù)調(diào)用應用類加載器的findClass(name)方法,同樣成功返回寿冕,失敗調(diào)用自定義類加載器的findClass(name)
- 我們自定義的類加載器一般會重寫findClass方法蕊程,使用自定義的類加載器加載一個父類加載器加載不了的類的時候,就會執(zhí)行自定義的findClass方法驼唱,在此方法中藻茂,會指定二進制字節(jié)碼的路徑讀入字節(jié)數(shù)組,最后調(diào)用defineClass返回加載成功的類
下面是自定義類加載器的示例代碼:
public class MyClassLoader extends ClassLoader{
private String classpath;
//指定父類加載器的構(gòu)造函數(shù)
public MyClassLoader(String classpath,ClassLoader classLoader) {
super(classLoader);
this.classpath = classpath;
}
//默認父類加載器為應用程序加載器的構(gòu)造函數(shù)
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
//重寫findClass玫恳,加載類文件辨赐,返回類
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFilePath = null;
String finalName = name.replace(".", "/");
classFilePath = classpath + "/" + finalName + ".class";
Path path = Paths.get(classFilePath);
if (!Files.exists(path)) {
return null;
}
try {
byte[] classData = Files.readAllBytes(path);
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new RuntimeException("Can not read class file into byte array");
}
}
}
為什么要使用這個模型?
最后來講講為什么要使用這個模型京办?用這個模型有什么好處掀序?
采用雙親委派模式的好處之一是類和它對應的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)系可以避免類的重復加載惭婿,當父類加載器已經(jīng)加載了該類時不恭,子類加載器就沒有必要再加載一次叶雹。
其次是考慮到安全因素,保證java核心api中定義的類型不會被隨意替換换吧,假設通過網(wǎng)絡傳遞一個名為java.lang.Integer的類折晦,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類沾瓦,發(fā)現(xiàn)該類已被加載满着,并不會重新加載網(wǎng)絡傳遞過來的java.lang.Integer,而直接返回已加載過的Integer.class贯莺,這樣便可以防止核心API庫被隨意篡改风喇。
原文鏈接:http://www.jackielee.cn/posts/7a3ae3d8.html
歡迎掃描關(guān)注我的公眾號