JVM理解(上):classloader載入class檔案的原理和機制
1 JVM架構整體架構
在進入classloader分析之前,先了解一下jvm整體架構:

JVM架構
JVM被分為三個主要的子系統
(1)類載入器子系統(2)執行時資料區(3)執行引擎
1. 類載入器子系統

Java的動態類載入功能是由類載入器子系統處理。當它在 執行時 (不是編譯時)首次引用一個類時,它載入、連結並初始化該類檔案。
1.1 載入: 類由此元件載入。啟動類載入器 (BootStrap class Loader)、擴充套件類載入器(Extension class Loader)和應用程式類載入器(Application class Loader) 這三種類載入器幫助完成類的載入。1. 啟動類載入器 – 負責從啟動類路徑中載入類,無非就是rt.jar。這個載入器會被賦予最高優先順序。2. 擴充套件類載入 器 – 負責載入ext 目錄(jre\lib)內的類.3. 應用程式類載入器 – 負責載入應用程式級別類路徑,涉及到路徑的環境變數等etc.上述的類載入器會遵循委託層次演算法(Delegation Hierarchy Algorithm)載入類檔案,這個在後面進行講解。
載入過程主要完成三件事情:
-
通過類的全限定名來獲取定義此類的二進位制位元組流
-
將這個類位元組流代表的 靜態儲存結構轉為方法區 的執行時資料結構
-
在 堆中生成一個代表此類的java.lang.Class物件 ,作為訪問方法區這些資料結構的入口。
1.2 連結:
- 校驗 位元組碼校驗器會校驗生成的位元組碼是否正確,如果校驗失敗,我們會得到校驗錯誤。
檔案格式驗證:基於位元組流驗證,驗證位元組流符合當前的Class檔案格式的規範,能被當前虛擬機器處理。驗證通過後,位元組流才會進入記憶體的方法區進行儲存。
元資料驗證:基於方法區的儲存結構驗證,對位元組碼進行語義驗證,確保不存在不符合java語言規範的元資料資訊。
位元組碼驗證:基於方法區的儲存結構驗證,通過對資料流和控制流的分析,保證被檢驗類的方法在執行時不會做出危害虛擬機器的動作。
符號引用驗證:基於方法區的儲存結構驗證,發生在解析階段,確保能夠將符號引用成功的解析為直接引用,其目的是確保解析動作正常執行。換句話說就是對類自身以外的資訊進行匹配性校驗。
- 準備 – 分配記憶體並初始化預設值給所有的靜態變數。
public static int value=33;
這據程式碼的賦值過程分兩次,一是上面我們提到的階段,此時的value將會被賦值為0;而value=33這個過程發生在類構造器的<clinit>()方法中。
- 解析所有符號記憶體引用被方法區(Method Area)的原始引用所替代。
舉個例子來說明,在com.sbbic.Person類中引用了com.sbbic.Animal類,在編譯階段,Person類並不知道Animal的實際記憶體地址,因此只能用com.sbbic.Animal來代表Animal真實的記憶體地址。在解析階段,JVM可以通過解析該符號引用,來確定com.sbbic.Animal類的真實記憶體地址(如果該類未被載入過,則先載入)。
主要有以下四種:類或介面的解析,欄位解析,類方法解析,介面方法解析
1.3 初始化: 這是類載入的最後階段,這裡所有的 靜態變數會被賦初始值 , 並且 靜態塊將被執行。
java中,對於初始化階段,有且只有**以下五種情況才會對要求類立刻初始化:
-
使用new關鍵字例項化物件、訪問或者設定一個類的靜態欄位(被final修飾、編譯器優化時已經放入常量池的例外)、呼叫類方法,都會初始化該靜態欄位或者靜態方法所在的類;
-
初始化類的時候,如果其父類沒有被初始化過,則要先觸發其父類初始化;
-
使用java.lang.reflect包的方法進行反射呼叫的時候,如果類沒有被初始化,則要先初始化;
-
虛擬機器啟動時,使用者會先初始化要執行的主類(含有main);
-
jdk 1.7後,如果java.lang.invoke.MethodHandle的例項最後對應的解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法控制代碼,並且這個方法所在類沒有初始化,則先初始化;
2.執行時資料區(Runtime Data Area)
The 執行時資料區域被劃分為5個主要元件:
① 方法區 (執行緒共享) 常量 靜態變數 JIT(即時編譯器)編譯後代碼也在方法區存放
② 堆記憶體(執行緒共享) 垃圾回收的主要場地
③ 程式計數器 當前執行緒執行的位元組碼的位置指示器
④ Java虛擬機器棧(棧記憶體) :儲存區域性變數,基本資料型別以及堆記憶體中物件的引用變數
⑤ 本地方法棧 (C棧):為JVM提供使用native方法的服務

3. 執行引擎
分配給執行時資料區的位元組碼將由執行引擎執行。執行引擎讀取位元組碼並逐段執行。3.1 直譯器 : 直譯器能快速的 解釋位元組碼 ,但執行卻很慢。 直譯器的缺點就是,當一個方法被呼叫多次,每次都需要重新解釋。
3.2 編譯器: JIT編譯器消除了直譯器的缺點。執行引擎利用直譯器轉換位元組碼,但如果是重複的程式碼則使用 JIT編譯器將全部位元組碼編譯成本機程式碼 。本機程式碼將直接用於重複的方法呼叫,這提高了系統的效能。a. 中間程式碼生成器– 生成中間程式碼b. 程式碼優化器– 負責優化上面生成的中間程式碼c. 目的碼生成器– 負責生成機器程式碼或本機程式碼d. 探測器(Profiler) – 一個特殊的元件,負責尋找被多次呼叫的方法。
3.3 垃圾回收器 : 收集並刪除未引用的物件。可以通過呼叫"System.gc()"來觸發垃圾回收,但並不保證會確實進行垃圾回收。JVM的垃圾回收只收集哪些由new關鍵字建立的物件。所以,如果不是用new建立的物件,你可以使用finalize函式來執行清理。Java本地介面 (JNI): JNI會與本地方法庫進行互動並提供執行引擎所需的本地庫。本地方法庫:它是一個執行引擎所需的本地庫的集合。
下面,通過一個小程式認識JVM:
package com.spark.jvm; /** * 從JVM呼叫的角度分析java程式堆記憶體空間的使用: * 當JVM程序啟動的時候,會從類載入路徑中找到包含main方法的入口類HelloJVM * 找到HelloJVM會直接讀取該檔案中的二進位制資料,並且把該類的資訊放到執行時的Method記憶體區域中。 * 然後會定位到HelloJVM中的main方法的位元組碼中,並開始執行Main方法中的指令 * 此時會建立Student例項物件,並且使用student來引用該物件(或者說給該物件命名),其內幕如下: * 第一步:JVM會直接到Method區域中去查詢Student類的資訊,此時發現沒有Student類,就通過類載入器載入該Student類檔案; * 第二步:在JVM的Method區域中載入並找到了Student類之後會在Heap區域中為Student例項物件分配記憶體, * 並且在Student的例項物件中持有指向方法區域中的Student類的引用(記憶體地址); * 第三步:JVM例項化完成後會在當前執行緒中為Stack中的reference建立實際的應用關係,此時會賦值給student * 接下來就是呼叫方法 * 在JVM中方法的呼叫一定是屬於執行緒的行為,也就是說方法呼叫本身會發生線上程的方法呼叫棧: * 執行緒的方法呼叫棧(Method Stack Frames),每一個方法的呼叫就是方法呼叫棧中的一個Frame, * 該Frame包含了方法的引數,區域性變數,臨時資料等 student.sayHello(); */ public class HelloJVM { //在JVM執行的時候會通過反射的方式到Method區域找到入口方法main public static void main(String[] args) {//main方法也是放在Method方法區域中的 /** * student(小寫的)是放在主執行緒中的Stack區域中的 * Student物件例項是放在所有執行緒共享的Heap區域中的 */ Student student = new Student("spark"); /** * 首先會通過student指標(或控制代碼)(指標就直接指向堆中的物件,控制代碼表明有一箇中間的,student指向控制代碼,控制代碼指向物件) * 找Student物件,當找到該物件後會通過物件內部指向方法區域中的指標來呼叫具體的方法去執行任務 */ student.sayHello(); } } class Student { // name本身作為成員是放在stack區域的但是name指向的String物件是放在Heap中 private String name; public Student(String name) { this.name = name; } //sayHello這個方法是放在方法區中的 public void sayHello() { System.out.println("Hello, this is " + this.name); } }
classloader載入class檔案的原理和機制
下面部分內容,整理自 ofollow,noindex">《深入分析JavaWeb技術內幕》
Classloader負責將Class載入到JVM中,並且確定由那個ClassLoader來載入(父優先的等級載入機制)。還有一個任務就是將Class位元組碼重新解釋為JVM統一要求的格式
1.Classloader 類結構分析
(1) 主要由四個方法,分別是 defineClass , findClass , loadClass , resolveClass
- <1>defineClass(byte[] , int ,int) 將byte位元組流解析為JVM能夠識別的Class物件(直接呼叫這個方法生成的Class物件還沒有resolve,這個resolve將會在這個物件真正例項化時resolve)
- <2>findClass,通過類名去載入對應的Class物件。當我們實現自定義的classLoader通常是重寫這個方法,根據傳入的類名找到對應位元組碼的檔案,並通過呼叫defineClass解析出Class獨享
- <3>loadClass執行時可以通過呼叫此方法載入一個類(由於類是動態載入進jvm,用多少載入多少的?)
- <4>resolveClass手動呼叫這個使得被加到JVM的類被連結(解析resolve這個類?)
(2) 實現自定義 ClassLoader 一般會繼承 URLClassLoader 類,因為這個類實現了大部分方法。
2. 常見載入類錯誤分析
(1)ClassNotFoundException :
通常是jvm要載入一個檔案的位元組碼到記憶體時,沒有找到這些位元組碼(如forName,loadClass等方法)
(2)NoClassDefFoundError :
通常是使用new關鍵字,屬性引用了某個類,繼承了某個類或介面,但JVM載入這些類時發現這些類不存在的異常
(3)UnsatisfiedLinkErrpr:
如native的方法找不到本機的lib
3. 常用 classLoader (書本此處其實是對 tomcat 載入 servlet 使用的 classLoader 分析)
(1)AppClassLoader :
載入jvm的classpath中的類和tomcat的核心類
(2)StandardClassLoader:
載入tomcat容器的classLoader,另外webAppClassLoader在loadclass時,發現類不在JVM的classPath下,在PackageTriggers(是一個字串陣列,包含一組不能使用webAppClassLoader載入的類的包名字串)下的話,將由該載入器載入(注意:StandardClassLoader並沒有覆蓋loadclass方法,所以其載入的類和AppClassLoader載入沒什麼分別,並且使用getClassLoader返回的也是AppClassLoader)(另外,如果web應用直接放在tomcat的webapp目錄下該應用就會通過StandardClassLoader載入,估計是因為webapp目錄在PackageTriggers中?)
(3)webAppClassLoader 如:
Servlet等web應用中的類的載入(loadclass方法的規則詳見P169)
4. 自定義的 classloader
(1) 需要使用自定義 classloader 的情況
- <1>不在System.getProperty("java.class.path")中的類檔案不可以被AppClassLoader找到(LoaderClass方法只會去classpath下載入特定類名的類),當class檔案的位元組碼不在ClassPath就需要自定義classloader
- <2>對載入的某些類需要作特殊處理
- <3>定義類的實效機制,對已經修改的類重新載入,實現熱部署
(2) 載入自定義路徑中的 class 檔案
- <1>載入特定來源的某些類:重寫find方法,使特定類或者特定來源的位元組碼 通過defineClass獲得class類並返回(應該符合jvm的類載入規範,其他類仍使用父載入器載入)
- <2>載入自頂一個是的class檔案(如經過網路傳來的經過加密的class檔案位元組碼):findclass中加密後再載入
5. 實現類的熱部署:
- (1)同一個classLoader的兩個例項載入同一個類,JVM也會識別為兩個
- (2)不能重複載入同一個類(全名相同,並使用同一個類載入器),會報錯
- (3)不應該動態載入類,因為物件唄引用後,物件的屬性結構被修改會引發問題
注意:使用不同classLoader載入的同一個類檔案得到的類,JVM將當作是兩個不同類,使用單例模式,強制型別轉換時都可能因為這個原因出問題。
6 類載入器的雙親委派模型
當一個類載入器收到一個類載入的請求,它首先會將該請求委派給父類載入器去載入,每一個層次的類載入器都是如此,因此所有的類載入請求最終都應該被傳入到頂層的啟動類載入器(Bootstrap ClassLoader)中,只有當父類載入器反饋無法完成這個列的載入請求時(它的搜尋範圍內不存在這個類),子類載入器才嘗試載入。其層次結構示意圖如下:

不難發現,該種載入流程的好處在於:
可以避免重複載入,父類已經載入了,子類就不需要再次載入
更加安全,很好的解決了各個類載入器的基礎類的統一問題,如果不使用該種方式,那麼使用者可以隨意定義類載入器來載入核心api,會帶來相關隱患。
接下來,我們看看雙親委派模型是如何實現的:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先先檢查該類已經被載入過了 Class c = findLoadedClass(name); if (c == null) {//該類沒有載入過,交給父類載入 long t0 = System.nanoTime(); try { if (parent != null) {//交給父類載入 c = parent.loadClass(name, false); } else {//父類不存在,則交給啟動類載入器載入 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //父類載入器丟擲異常,無法完成類載入請求 } if (c == null) {// long t1 = System.nanoTime(); //父類載入器無法完成類載入請求時,呼叫自身的findClass方法來完成類載入 c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
這裡有些童鞋會問,JVM怎麼知道一個某個類載入器的父載入器呢?如果你有此疑問,請重新再看一遍.
7 類載入器的特點
執行任何一個程式時,總是由Application Loader開始載入指定的類。
一個類在收到載入類請求時,總是先交給其父類嘗試載入。
Bootstrap Loader是最頂級的類載入器,其父載入器為null。
8 類載入的三種方式
通過命令列啟動應用時由JVM初始化載入含有main()方法的主類。
通過Class.forName()方法動態載入,會預設執行初始化塊(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要執行初始化塊。
通過ClassLoader.loadClass()方法動態載入,不會執行初始化塊。
9 自定義類載入器的兩種方式
1、遵守雙親委派模型:繼承ClassLoader,重寫findClass()方法。 2、破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。 通常我們推薦採用第一種方法自定義類載入器,最大程度上的遵守雙親委派模型。 自定義類載入的目的是想要手動控制類的載入,那除了通過自定義的類載入器來手動載入類這種方式,還有其他的方式麼?
利用現成的類載入器進行載入:
1. 利用當前類載入器
Class.forName();
2. 通過系統類載入器
Classloader.getSystemClassLoader().loadClass();
3. 通過上下文類載入器
Thread.currentThread().getContextClassLoader().loadClass();
l 利用URLClassLoader進行載入:
URLClassLoader loader=new URLClassLoader(); loader.loadClass();
類載入例項演示: 命令列下執行HelloWorld.java
public class HelloWorld{ public static void main(String[] args){ System.out.println("Hello world"); } }
該段程式碼大體經過了一下步驟:
-
尋找jre目錄,尋找jvm.dll,並初始化JVM.
-
產生一個Bootstrap ClassLoader;
-
Bootstrap ClassLoader載入器會載入他指定路徑下的java核心api,並且生成Extended ClassLoader載入器的例項,然後Extended ClassLoader會載入指定路徑下的擴充套件java api,並將其父設定為Bootstrap ClassLoader。
-
Bootstrap ClassLoader生成Application ClassLoader,並將其父Loader設定為Extended ClassLoader。
-
最後由AppClass ClassLoader載入classpath目錄下定義的類——HelloWorld類。
我們上面談到 Extended ClassLoader和Application ClassLoader是通過Launcher來建立,現在我們再看看原始碼:
public Launcher() { Launcher.ExtClassLoader var1; try { //例項化ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //例項化AppClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } //主執行緒設定預設的Context ClassLoader為AppClassLoader. //因此在主執行緒中建立的子執行緒的Context ClassLoader 也是AppClassLoader Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if(var2 != null) { SecurityManager var3 = null; if(!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { ; } catch (InstantiationException var6) { ; } catch (ClassNotFoundException var7) { ; } catch (ClassCastException var8) { ; } } else { var3 = new SecurityManager(); } if(var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
10 非常重要
在這裡呢我們需要注意幾個問題:
1. 我們知道ClassLoader通過一個類的全限定名來獲取二進位制流,那麼如果我們需要通過自定義類載入其來載入一個Jar包的時候,難道要自己遍歷jar中的類,然後依次通過ClassLoader進行載入嗎?或者說我們怎麼來載入一個jar包呢?
2. 如果一個類引用的其他的類,那麼這個其他的類由誰來載入?
3. 既然類可以由不同的載入器載入,那麼如何確定兩個類如何是同一個類?
我們來依次解答這兩個問題: 對於動態載入jar而言,JVM預設會使用第一次載入該jar中指定類的類載入器作為預設的ClassLoader.假設我們現在存在名為sbbic的jar包,該包中存在ClassA和ClassB這兩個類(ClassA中沒有引用ClassB).現在我們通過自定義的ClassLoaderA來載入在ClassA這個類,那麼此時此時ClassLoaderA就成為sbbic.jar中其他類的預設類載入器.也就是,ClassB也預設會通過ClassLoaderA去載入.
那麼如果ClassA中引用了ClassB呢?當類載入器在載入ClassA的時候,發現引用了ClassB,此時類載入如果檢測到ClassB還沒有被載入,則先回去載入.當ClassB載入完成後,繼續回來載入ClassA.換句話說,類會通過自身對應的來載入其載入其他引用的類.
JVM規定,對於任何一個類,都需要由載入它的類載入器和這個類本身一同確立在java虛擬機器中的唯一性,通俗點就是說,在jvm中判斷兩個類是否是同一個類取決於類載入和類本身,也就是同一個類載入器載入的同一份Class檔案生成的Class物件才是相同的,類載入器不同,那麼這兩個類一定不相同.