1. 程式人生 > >圖解JVM類載入機制與類載入過程

圖解JVM類載入機制與類載入過程

0、前言

讀完本文,你將瞭解到:

一、為什麼說Jabalpur語言是跨平臺的

二、Java虛擬機器啟動、載入類過程分析

三、類載入器有哪些?其組織結構是怎樣的?

四、雙親載入模型的邏輯和底層程式碼實現是怎樣的?

五、類載入器與Class<T>  例項的關係

六、執行緒上下文載入器

一、為什麼說Java語言是跨平臺的?

  Java語言之所以說它是跨平臺的、可以在當前絕大部分的作業系統平臺下執行,是因為Java語言的執行環境是在Java虛擬機器中。

  Java虛擬機器消除了各個平臺之間的差異,只要作業系統平臺下安裝了Java虛擬機器,那麼使用Java開發的東西都能在其上面執行。如下圖所示:

這裡寫圖片描述

  Java虛擬機器對各個平臺而言,實質上是各個平臺上的一個可執行程式。例如在windows平臺下,java虛擬機器對於windows而言,就是一個java.exe程序而已。

二、Java虛擬機器啟動、載入類過程分析

  下面我將定義一個非常簡單的java程式並執行它,來逐步分析java虛擬機器啟動的過程。

package org.luanlouis.jvm.load;  
import sun.security.pkcs11.P11Util;  

/** 
 * Created by louis on 2016/1/16. 
 */  
public class Main{
public static void main(String[] args) { System.out.println(”Hello,World!”); ClassLoader loader = P11Util.class.getClassLoader(); System.out.println(loader); } }

  在windows命令列下輸入:

java org.luanlouis.jvm.load.Main

  當輸入上述的命令時,windows開始執行{JRE_HOME}/bin/java.exe程式,java.exe 程式將完成以下步驟:

1. 根據JVM記憶體配置要求,為JVM申請特定大小的記憶體空間

2. 建立一個引導類載入器例項,初步載入系統類到記憶體方法區區域中

3.建立JVM 啟動器例項 Launcher,並取得類載入器ClassLoader

4.使用上述獲取的ClassLoader例項載入我們定義的 org.luanlouis.jvm.load.Main類

5.載入完成時候JVM會執行Main類的main方法入口,執行Main類的main方法

6. 結束,java程式執行結束,JVM銷燬

Step 1.根據JVM記憶體配置要求,為JVM申請特定大小的記憶體空間

  為了不降低本文的理解難度,這裡就不詳細介紹JVM記憶體配置要求的話題,今概括地介紹一下記憶體的功能劃分。

  JVM啟動時,按功能劃分,其記憶體應該由以下幾部分組成:

這裡寫圖片描述

  如上圖所示,JVM記憶體按照功能上的劃分,可以粗略地劃分為方法區(Method Area)堆(Heap),而所有的類的定義資訊都會被載入到方法區中。

Step 2. 建立一個引導類載入器例項,初步載入系統類到記憶體方法區區域中;

  JVM申請好記憶體空間後,JVM會建立一個引導類載入器(Bootstrap Classloader)例項,引導類載入器是使用C++語言實現的,負責載入JVM虛擬機器執行時所需的基本系統級別的類,如java.lang.String, java.lang.Object等等。

  引導類載入器(Bootstrap Classloader)會讀取 {JRE_HOME}/lib下的jar包和配置,然後將這些系統類載入到方法區內。

  本例中,引導類載入器是用 {JRE_HOME}/lib載入類的,不過,你也可以使用引數 -Xbootclasspath 或系統變數sun.boot.class.path來指定的目錄來載入類。

  一般而言,{JRE_HOME}/lib下存放著JVM正常工作所需要的系統類,如下表所示:

檔名 描述
rt.jar 執行環境包,rt即runtime,J2SE 的類定義都在這個包內
charsets.jar 字符集支援包
jce.jar 是一組包,它們提供用於加密、金鑰生成和協商以及 Message Authentication Code(MAC)演算法的框架和實現
jsse.jar 安全套接字拓展包Java(TM) Secure Socket Extension
classlist 該檔案內表示是引導類載入器應該載入的類的清單
net.properties JVM 網路配置資訊

  引導類載入器(Bootstrap ClassLoader)載入系統類後,JVM記憶體會呈現如下格局:

這裡寫圖片描述

  引導類載入器將類資訊載入到方法區中,以特定方式組織,對於某一個特定的類而言,在方法區中它應該有執行時常量池、型別資訊、欄位資訊、方法資訊、類載入器的引用對應class例項的引用等資訊。

  類載入器的引用,由於這些類是由引導類載入器(Bootstrap Classloader)進行載入的,而 引導類載入器是有C++語言實現的,所以是無法訪問的,故而該引用為NULL

  對應class例項的引用, 類載入器在載入類資訊放到方法區中後,會建立一個對應的Class 型別的例項放到堆(Heap)中, 作為開發人員訪問方法區中類定義的入口和切入點。

  小測試,當我們在程式碼中嘗試獲取系統類如java.lang.Object的類載入器時,你會始終得到NULL

System.out.println(String.class.getClassLoader());//null  
System.out.println(Object.class.getClassLoader());//null  
System.out.println(Math.class.getClassLoader());//null  
System.out.println(System.class.getClassLoader());//null  

Step 3. 建立JVM 啟動器例項 Launcher,並取得類載入器ClassLoader

  上述步驟完成,JVM基本執行環境就準備就緒了。接著,我們要讓JVM工作起來了:執行我們定義的程式 org.luanlouis,jvm.load.Main。

  此時,JVM虛擬機器呼叫已經載入在方法區的類sun.misc.Launcher 的靜態方法getLauncher() 獲取sun.misc.Launcher 例項:

sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); //獲取Java啟動器  
ClassLoader classLoader = launcher.getClassLoader();          //獲取類載入器ClassLoader用來載入class到記憶體來

  sun.misc.Launcher使用了單例模式設計,保證一個JVM虛擬機器內只有一個sun.misc.Launcher例項。在Launcher的內部,其定義了兩個類載入器(ClassLoader),分別是sun.misc.Launcher.ExtClassLoader和sun.misc.Launcher.AppClassLoader,這兩個類載入器分別被稱為拓展類載入器(Extension ClassLoader)和應用類載入器(Application ClassLoader)。如下圖所示:

  圖例註釋:除了引導類載入器(Bootstrap Class Loader )的所有類載入器,都有一個能力,就是判斷某一個類是否被引導類載入器載入過,如果載入過,可以直接返回對應的Class<T> instance,如果沒有,則返回null.  圖上的指向引導類載入器的虛線表示類載入器的這個有限的訪問 引導類載入器的功能。

  此時的launcher.getClassLoader()方法將會返回AppClassLoader例項,AppClassLoader將ExtClassLoader作為自己的父載入器。

  當ppClassLoader載入類時,會首先嚐試讓父載入器ExtClassLoader進行載入,如果父載入器ExtClassLoader載入成功,則AppClassLoader直接返回父載入器ExtClassLoader載入的結果;如果父載入器ExtClassLoader載入失敗,AppClassLoader則會判斷該類是否是引導的系統類(即是否是通過Bootstrap類載入器載入,這會呼叫Native方法進行查詢);若要載入的類不是系統引導類,那麼ClassLoader將會嘗試自己載入,載入失敗將會丟擲“ClassNotFoundException”。

  具體AppClassLoader的工作流程如下所示:

  雙親委派模型(parent-delegation model)

  上面討論的應用類載入器AppClassLoader的載入類的模式就是我們常說的雙親委派模型(parent-delegation model)。對於某個特定的類載入器而言,應該為其指定一個父類載入器,當用其進行載入類的時候:

  1. 委託父類載入器幫忙載入;
  2. 父類載入器載入不了,則查詢引導類載入器有沒有載入過該類;
  3. 如果引導類載入器沒有載入過該類,則當前的類載入器應該自己載入該類;
  4. 若載入成功,返回 對應的Class<T> 物件;若失敗,丟擲異常“ClassNotFoundException”。

  請注意

  雙親委派模型中的”雙親”並不是指它有兩個父類載入器的意思,一個類載入器只應該有一個父載入器。上面的步驟中,有兩個角色:

  1. 父類載入器(parent classloader):它可以替子載入器嘗試載入類
  2. 引導類載入器(bootstrap classloader): 子類載入器只能判斷某個類是否被引導類載入器載入過,而不能委託它載入某個類;換句話說,就是子類載入器不能接觸到引導類載入器,引導類載入器對其他類載入器而言是透明的。

  一般情況下,雙親載入模型如下所示:

Step 4. 使用類載入器ClassLoader載入Main類

  通過 launcher.getClassLoader()方法返回AppClassLoader例項,接著就是AppClassLoader載入 org.luanlouis.jvm.load.Main類的時候了。

ClassLoader classloader = launcher.getClassLoader();//取得AppClassLoader類  
classLoader.loadClass(”org.luanlouis.jvm.load.Main”);//載入自定義類  

  上述定義的org.luanlouis.jvm.load.Main類被編譯成org.luanlouis.jvm.load.Main class二進位制檔案,這個class檔案中有一個叫常量池(Constant Pool)的結構體來儲存該class的常亮資訊。常量池中有CONSTANT_CLASS_INFO型別的常量,表示該class中聲明瞭要用到那些類:

  當AppClassLoader要載入 org.luanlouis.jvm.load.Main類時,會去檢視該類的定義,發現它內部宣告使用了其它的類: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;org.luanlouis.jvm.load.Main類要想正常工作,首先要能夠保證這些其內部宣告的類載入成功。所以AppClassLoader要先將這些類載入到記憶體中。(注:為了理解方便,這裡沒有考慮懶載入的情況,事實上的JVM載入類過程比這複雜的多)

  載入順序:

  1. 載入java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class

  AppClassLoader嘗試載入這些類的時候,會先委託ExtClassLoader進行載入;而ExtClassLoader發現不是其載入範圍,其返回null;AppClassLoader發現父類載入器ExtClassLoader無法載入,則會查詢這些類是否已經被BootstrapClassLoader載入過,結果表明這些類已經被BootstrapClassLoader載入過,則無需重複載入,直接返回對應的Class<T>例項;

  2. 載入sun.security.pkcs11.P11Util

   此在{JRE_HOME}/lib/ext/sunpkcs11.jar包內,屬於ExtClassLoader負責載入的範疇。AppClassLoader嘗試載入這些類的時候,會先委託ExtClassLoader進行載入;而ExtClassLoader發現其正好屬於載入範圍,故ExtClassLoader負責將其載入到記憶體中。ExtClassLoader在載入sun.security.pkcs11.P11Util時也分析這個類內都使用了哪些類,並將這些類先載入記憶體後,才開始載入sun.security.pkcs11.P11Util,載入成功後直接返回對應的Class<sun.security.pkcs11.P11Util>例項;

  3. 載入org.luanlouis.jvm.load.Main

  AppClassLoader嘗試載入這些類的時候,會先委託ExtClassLoader進行載入;而ExtClassLoader發現不是其載入範圍,其返回null;AppClassLoader發現父類載入器ExtClassLoader無法載入,則會查詢這些類是否已經被BootstrapClassLoader載入過。而結果表明BootstrapClassLoader 沒有載入過它,這時候AppClassLoader只能自己動手負責將其載入到記憶體中,然後返回對應的Class<org.luanlouis.jvm.load.Main>例項引用;

  以上三步驟都成功,才表示classLoader.loadClass(“org.luanlouis.jvm.load.Main”)完成,上述操作完成後,JVM記憶體方法區的格局會如下所示:

  如上圖所示:

  JVM方法區的類資訊區是按照類載入器進行劃分的,每個類載入器會維護自己載入類資訊;

  某個類載入器在載入相應的類時,會相應地在JVM記憶體堆(Heap)中建立一個對應的Class<T>,用來表示訪問該類資訊的入口

Step 5. 使用Main類的main方法作為程式入口執行程式

Step 6. 方法執行完畢,JVM銷燬,釋放記憶體

三、類載入器有哪些?其組織結構是怎樣的?

  類載入器(Class Loader):顧名思義,指的是可以載入類的工具。JVM自身定義了三個類載入器:引導類載入器(Bootstrap Class Loader)、拓展類載入器(Extension Class Loader )、應用載入器(Application Class Loader)。當然,我們有時候也會自己定義一些類載入器來滿足自身的需要。

  引導類載入器(Bootstrap Class Loader): 該類載入器使JVM使用C/C++底層程式碼實現的載入器,用以載入JVM執行時所需要的系統類,這些系統類在{JRE_HOME}/lib目錄下。由於類載入器是使用平臺相關的底層C/C++語言實現的, 所以該載入器不能被Java程式碼訪問到。但是,我們可以查詢某個類是否被引導類載入器載入過。我們經常使用的系統類如:java.lang.String,java.lang.Object,java.lang*……. 這些都被放在 {JRE_HOME}/lib/rt.jar包內, 當JVM系統啟動的時候,引導類載入器會將其載入到 JVM記憶體的方法區中。

   拓展類載入器(Extension Class Loader): 該載入器是用於載入 java 的拓展類 ,拓展類一般會放在 {JRE_HOME}/lib/ext/ 目錄下,用來提供除了系統類之外的額外功能。拓展類載入器是是整個JVM載入器的Java程式碼可以訪問到的類載入器的最頂端,即是超級父載入器,拓展類載入器是沒有父類載入器的。

  應用類載入器(Applocatoin Class Loader): 該類載入器是用於載入使用者程式碼,是使用者程式碼的入口。我經常執行指令 java   xxx.x.xxx.x.x.XClass , 實際上,JVM就是使用的AppClassLoader載入 xxx.x.xxx.x.x.XClass 類的。應用類載入器將拓展類載入器當成自己的父類載入器,當其嘗試載入類的時候,首先嚐試讓其父載入器-拓展類載入器載入;如果拓展類載入器載入成功,則直接返回載入結果Class<T> instance,載入失敗,則會詢問是否引導類載入器已經載入了該類;只有沒有載入的時候,應用類載入器才會嘗試自己載入。由於xxx.x.xxx.x.x.XClass是整個使用者程式碼的入口,在Java虛擬機器規範中,稱其為 初始類(Initial Class)。

   使用者自定義類載入器(Customized Class Loader):使用者可以自己定義類載入器來載入類。所有的類載入器都要繼承java.lang.ClassLoader類。

四、雙親載入模型的邏輯和底層程式碼實現是怎樣的?

  上面已經不厭其煩地講解什麼是雙親載入模型,以及其機制是什麼,這些東西都是可以通過底層程式碼檢視到的。

  我們也可以通過JDK原始碼看java.lang.ClassLoader的核心方法 loadClass()的實現:

//提供class類的二進位制名稱表示,載入對應class,載入成功,則返回表示該類對應的Class<T> instance 例項  
public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  


protected Class<?> loadClass(String name, boolean resolve)  
    throws ClassNotFoundException  
{  
    synchronized (getClassLoadingLock(name)) {  
        // 首先,檢查是否已經被當前的類載入器記載過了,如果已經被載入,直接返回對應的Class<T>例項  
        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) {  
                // ClassNotFoundException thrown if class not found  
                // from the non-null parent class loader  
            }  
            // 父載入器載入失敗,並且沒有被引導類載入器載入,則嘗試該類載入器自己嘗試載入  
            if (c == null) {  
                // If still not found, then invoke findClass in order  
                // to find the class.  
                long t1 = System.nanoTime();  
                // 自己嘗試載入  
                c = findClass(name);  

                // this is the defining class loader; record the stats  
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);  
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
                sun.misc.PerfCounter.getFindClasses().increment();  
            }  
        }  
        //是否解析類   
        if (resolve) {  
            resolveClass(c);  
        }  
        return c;  
    }  
}  

  相對應地,我們可以整理出雙親模型的工作流程圖:

  相信讀者看過這張圖後會對雙親載入模型有了非常清晰的脈絡。當然,這是JDK自身預設的載入類的行為,我們可以通過繼承複寫該方法,改變其行為。

五、類載入器與Class<T>  例項的關係

六、執行緒上下文載入器

  Java 任何一段程式碼的執行,都有對應的執行緒上下文。如果我們在程式碼中,想看當前是哪一個執行緒在執行當前程式碼,我們經常是使用如下方法:

Thread  thread = Thread.currentThread();//返回對當當前執行執行緒的引用 

  相應地,我們可以為當前的執行緒指定類載入器。在上述的例子中, 當執行:

java org.luanlouis.jvm.load.Main
的時候,JVM會建立一個Main執行緒,而建立應用類載入器AppClassLoader的時候,會將AppClassLoader設定成Main執行緒的上下文類載入器:
public Launcher() {  
      Launcher.ExtClassLoader var1;  
      try {  
          var1 = Launcher.ExtClassLoader.getExtClassLoader();  
      } catch (IOException var10) {  
          throw new InternalError(“Could not create extension class loader”, var10);  
      }  

      try {  
          this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);  
      } catch (IOException var9) {  
          throw new InternalError(“Could not create application class loader”, var9);  
      }  
//將AppClassLoader設定成當前執行緒的上下文載入器  
      Thread.currentThread().setContextClassLoader(this.loader);  
      //…….  

  }  

  執行緒上下文類載入器是從執行緒的角度來看待類的載入,為每一個執行緒繫結一個類載入器,可以將類的載入從單純的 雙親載入模型解放出來,進而實現特定的載入需求。