1. 程式人生 > >(二十七)JVM類加載器機制與類加載過程

(二十七)JVM類加載器機制與類加載過程

有時 重復加載 win ppc context 類的定義 字符集 area main方法

一、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銷毀。

1.1Step 1 根據JVM內存配置要求,為JVM申請特定大小的內存空間

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

JVM啟動時,按功能劃分,其內存應該由以下幾部分組成:
技術分享圖片
如上圖所示,JVM內存按照功能上的劃分,可以粗略地劃分為方法區(Method Area) 和堆(Heap),而所有的類的定義信息都會被加載到方法區中。

1.2 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  

1.3 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.ExtClassLoadersun.misc.Launcher.AppClassLoader,這兩個類加載器分別被稱為拓展類加載器(Extension ClassLoader) 和 應用類加載器(Application ClassLoader).如下圖所示:

技術分享圖片

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

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

當AppClassLoader加載類時,會首先嘗試讓父加載器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): 子類加載器只能判斷某個類是否被引導類加載器加載過,而不能委托它加載某個類;換句話說,就是子類加載器不能接觸到引導類加載器,引導類加載器對其他類加載器而言是透明的。

一般情況下,雙親加載模型如下所示:
技術分享圖片


1.4 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>,用來表示訪問該類信息的入口

1.5 Step 5. 使用Main類的main方法作為程序入口運行程序

1.6 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);  
      //.......  
  
  }  

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

(二十七)JVM類加載器機制與類加載過程