JVM類加載器機制與類加載過程

    該文章是我轉載的,不過我對文中的一些地方有不一樣的理解,因此修改幾處,還請做者見諒。java

    原貼:http://m.blog.csdn.net/article/details?id=50529868算法

 

讀完本文,你將瞭解到:

1、爲何說Java語言是跨平臺的bootstrap

2、Java虛擬機啓動、加載類過程分析windows

3、類加載器有哪些?其組織結構是怎樣的?安全

4、雙親加載模型的邏輯和底層代碼實現是怎樣的?網絡

5、類加載器與Class<T>  實例的關係app

6、線程上下文加載器框架

 

1、爲何說Java語言是跨平臺的?

 

Java語言之因此說它是跨平臺的、能夠在當前絕大部分的操做系統平臺下運行,是由於Java語言的運行環境是在Java虛擬機中。 
Java虛擬機消除了各個平臺之間的差別,只要操做系統平臺下安裝了Java虛擬機,那麼使用Java開發的東西都能在其上面運行。以下圖所示:jvm

這裏寫圖片描述

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

 

2、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),而全部的類的定義信息都會被加載到 方法區中。

關於具體方法區裏有什麼內容,讀者能夠參考個人另外一篇博文: 
《Java虛擬機原理圖解》三、JVM運行時數據區

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

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

      此時的  launcher.getClassLoader() 方法將會返回 AppClassLoader 實例,AppClassLoaderExtClassLoader做爲本身的父加載器。

    類加載器加載類的部分源碼:

//提供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) {
                        //若是有父類加載器,則先讓父類加載器加載,父類加載器的loadClass方法也是當前的loadClass方法
                        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;
        }
    }

由源碼可知,當AppClassLoader加載X類時的加載過程(這部分我修改過):

一、會先查找本身是否加載過X類,查找成功則返回Class<X>的實例引用;

二、若是查找失敗返後,會嘗試讓父加載器ExtClassLoader進行加載;

三、父加載器ExtClassLoader會先查找本身是否加載過X類,若是查找成功則返回Class<X>的實例引用,AppClassLoader也直接返回父加載器ExtClassLoader返回的實例引用

四、若是父加載器ExtClassLoader沒加載過該類,且父加載器ExtClassLoader沒有父加載器,因此查看Bootstrap類加載器是否加載過,若是加載過,返回Class<X>的實例引用;

五、若是Bootstrap類加載器沒有加載過,則父加載器ExtClassLoader在本身的加載範圍{JRE_HOME}/lib/ext中嘗試加載,若是加載成功,返回Class<X>的實例引用;

六、若是Bootstrap類加載器加載失敗將會拋出「ClassNotFoundException」,異常處理中什麼都不作;

七、最後AppClassLoader在本身的加載範圍classpath(經過System.getProperty("java.class.path")查看該路徑)中嘗試加載,若是加載成功,返回Class<X>的實例引用;

八、若是AppClassLoader加載失敗,則最終拋出「ClassNotFoundException」,

 

雙親委派模型(parent-delegation model):
上面討論的應用類加載器AppClassLoader的加載類的模式就是咱們常說的雙親委派模型(parent-delegation model).
請注意:
雙親委派模型中的"雙親"並非指它有兩個父類加載器的意思,一個類加載器只應該有一個父加載器。上面的步驟中,有兩個角色:
1. 父類加載器(parent classloader):它能夠替子加載器嘗試加載類
2. 引導類加載器(bootstrap classloader): 子類加載器只能判斷某個類是否被引導類加載器加載過,而不能委託它加載某個類;換句話說,就是子類加載器不能接觸到引導類加載器,引導類加載器對其餘類加載器而言是透明的。(由此可知:引導類加載器應該會在JVM啓動時就將全部有可能用到的系統基礎類都加載到內存,由於引導類加載器只會加載一次。而ext下的擴展類和classpath下的類則是按需加載或者按其餘方式加載;PS:項目中導入的Jar包是由AppClassLoader加載的)

通常狀況下,雙親加載模型以下所示:

 

 

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

      (這幾個類在{JRE_HOME}/lib下,在JVM啓動時就由引導類加載器加載過)

     加載原理和上面的加載原理(紅字體部分)相同,最後在上面的步驟4中(在引導類加載器加載過的類中找到),而後返回Class<T>實例引用

2. 加載sun.security.pkcs11.P11Util

    此類在{JRE_HOME}/lib/ext/sunpkcs11.jar包內,屬於ExtClassLoader負責加載的範疇。在上面的步驟3中(ExtClassLoader已經加載過的類中)找到,查找成功後直接返回對應的Class<sun.security.pkcs11.P11Util>實例;

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

  該類是在這AppClassLoader的負責範圍,因此在上面步驟的前6點都加載不成功。因此AppClassLoader只能本身動手負責將其加載到內存中(步驟7),例子中的Mian類是能夠在此處加載到的,最後返回對應的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銷燬,釋放內存

 

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

             類加載器(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 類的因爲xxx.x.xxx.x.x.XClass是整個用戶代碼的入口,在Java虛擬機規範中,稱其爲 初始類(Initial Class).

    
           用戶自定義類加載器(Customized Class Loader):用戶能夠本身定義類加載器來加載類。全部的類加載器都要繼承java.lang.ClassLoader類。

           

 

4、雙親加載模型的邏輯和底層代碼實現是怎樣的?

            上面已經不厭其煩地講解什麼是雙親加載模型,以及其機制是什麼,這些東西都是能夠經過底層代碼查看到的。

             咱們也能夠經過JDK源碼看java.lang.ClassLoader的核心方法 loadClass()的實現:代碼在上面已經給出

            相信讀者看過這張圖後會對雙親加載模型有了很是清晰的脈絡。固然,這是JDK自身默認的加載類的行爲,咱們能夠經過繼承複寫該方法,改變其行爲。

 

 

5、類加載器與Class<T>  實例的關係

 

 

 

6、線程上下文加載器

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

    }
線程上下文類加載器是從線程的角度來看待類的加載,爲每個線程綁定一個類加載器,能夠將類的加載從單純的 雙親加載模型解放出來,進而實現特定的加載需求。
相關文章
相關標籤/搜索