天天看點

JVM了解classloader加載class檔案的原理和機制

作者:wlteck

1 JVM架構整體架構

在進入classloader分析之前,先了解一下jvm整體架構:

JVM了解classloader加載class檔案的原理和機制

JVM架構

JVM被分為三個主要的子系統

(1)類加載器子系統(2)運作時資料區(3)執行引擎

1. 類加載器子系統

JVM了解classloader加載class檔案的原理和機制

Java的動态類加載功能是由類加載器子系統處理。當它在運作時(不是編譯時)首次引用一個類時,它加載、連結并初始化該類檔案。

1.1 加載:類由此元件加載。啟動類加載器 (BootStrap class Loader)、擴充類加載器(Extension class Loader)和應用程式類加載器(Application class Loader) 這三種類加載器幫助完成類的加載。1. 啟動類加載器 – 負責從啟動類路徑中加載類,無非就是rt.jar。這個加載器會被賦予最高優先級。2. 擴充類加載器 – 負責加載ext 目錄(jrelib)内的類.3. 應用程式類加載器 – 負責加載應用程式級别類路徑,涉及到路徑的環境變量等etc.上述的類加載器會遵循委托層次算法(Delegation Hierarchy Algorithm)加載類檔案,這個在後面進行講解。

加載過程主要完成三件事情:

  • 通過類的全限定名來擷取定義此類的二進制位元組流
  • 将這個類位元組流代表的靜态存儲結構轉為方法區的運作時資料結構
  • 在堆中生成一個代表此類的java.lang.Class對象,作為通路方法區這些資料結構的入口。

1.2 連結:

  1. 校驗 位元組碼校驗器會校驗生成的位元組碼是否正确,如果校驗失敗,我們會得到校驗錯誤。

檔案格式驗證:基于位元組流驗證,驗證位元組流符合目前的Class檔案格式的規範,能被目前虛拟機處理。驗證通過後,位元組流才會進入記憶體的方法區進行存儲。

中繼資料驗證:基于方法區的存儲結構驗證,對位元組碼進行語義驗證,確定不存在不符合java語言規範的中繼資料資訊。

位元組碼驗證:基于方法區的存儲結構驗證,通過對資料流和控制流的分析,保證被檢驗類的方法在運作時不會做出危害虛拟機的動作。

符号引用驗證:基于方法區的存儲結構驗證,發生在解析階段,確定能夠将符号引用成功的解析為直接引用,其目的是確定解析動作正常執行。換句話說就是對類自身以外的資訊進行比對性校驗。

JVM了解classloader加載class檔案的原理和機制
  1. 準備 – 配置設定記憶體并初始化預設值給所有的靜态變量。

public static int value=33;

這據代碼的指派過程分兩次,一是上面我們提到的階段,此時的value将會被指派為0;而value=33這個過程發生在類構造器的<clinit>()方法中。

  1. 解析所有符号記憶體引用被方法區(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方法的服務

JVM了解classloader加載class檔案的原理和機制

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檔案的原理和機制

下面部分内容,整理自《深入分析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)中,隻有當父類加載器回報無法完成這個列的加載請求時(它的搜尋範圍内不存在這個類),子類加載器才嘗試加載。其層次結構示意圖如下:

JVM了解classloader加載class檔案的原理和機制

不難發現,該種加載流程的好處在于:

可以避免重複加載,父類已經加載了,子類就不需要再次加載

更加安全,很好的解決了各個類加載器的基礎類的統一問題,如果不使用該種方式,那麼使用者可以随意定義類加載器來加載核心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對象才是相同的,類加載器不同,那麼這兩個類一定不相同.

繼續閱讀