虛擬機類加載機制

概念區分:
加載、類加載、類加載器java

類加載是一個過程。
加載(Loading)是類加載這一個過程的階段。
類加載器是ClassLoader類或其子類。 bootstrap

本文中的」類「的描述都包括了類和接口的可能性,由於每一個Class文件都有可能表明Java語言中的一個類或接口。
本文中的」Class文件「並不是特指存在於具體磁盤中的文件,更準確理解應該是一串二進制的字節流。安全

類加載過程分爲:網絡

  1. 加載 Loading(注意,別與類加載混淆,類加載是個過程,加載是其一個階段)
  2. 驗證 Verification
  3. 準備 Preparation
  4. 解析 Resolution
  5. 初始化 Initialization

加載

在這個階段,主要完成3件事:數據結構

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。 不必定要從本地的Class文件獲取,能夠從jar包,網絡,甚至十六進制編輯器弄出來的。開發人員能夠重寫類加載器的loadClass()方法。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生產一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口

驗證

這一階段目的爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。多線程

  1. 文件格式驗證,如魔數(0xCAFEBABE)開頭、主次版本號是否在當前虛擬機處理範圍以內等。
  2. 元數據驗證,此階段開始就不是直接操做字節流,而是讀取方法區裏的信息,元數據驗證大概就是驗證是否符合Java語言規範
  3. 字節碼驗證,是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是否合法,符合邏輯。JDK6以後作了優化,不在驗證,能夠經過-XX:-UseSplitVerifier關閉優化。
  4. 符號引用驗證,此階段能夠看作是類本身身意外的信息進行匹配性校驗。

準備

此階段正是爲 類變量 分配內存和設置 類變量 初始值的階段。這些變量所使用的內存都將在方法區中進行分配。注意這裏僅包括 類變量(被static修飾的變量),而不是包括實例變量。app

public static int value = 123;

在這個階段中,value的值是0jvm

如下是基本數據類型的零值編輯器

數據類型 零值 數據類型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char 'u0000' reference null
byte (byte)0

特殊狀況ide

public static final int value = 123;

編譯時javac將會爲value生產ConstantValue屬性,在準備階段虛擬機會根據ConstatnValue的設置,將value賦值爲123;。

解析

這個階段有點複雜,我還講不清,先跳過。 //TODO 2017年10月29日

初始化

類初始化是類加載過程的最後一步。在前面的類加載過程當中,除了在加載階段,用戶應用程序能夠經過本身定義類加載參與以外,其他動做徹底由虛擬機主導和控制。

到了這個初始化階段,才真正開始執行類中定義的Java程序代碼(或者說是字節碼)。

在編譯時,編譯器或自動收集 類 中的全部類變量(被static修飾的變量)的賦值操做和靜態語句塊中的語句合併,從而生成出一個叫<cinit>()方法。編譯器的收集順序是源文件中出現的順序決定的。也就是說靜態賦值語句和靜態代碼塊都是從上往下執行。

<cinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不一樣,他不須要顯示地調用父類構造器,虛擬機會保證子類的<cinit>()方法執行以前,父類的<cinit>()方法語句執行完畢。 這就意味着父類定義的靜態賦值語句和靜態代碼塊要優先於子類執行。

staic class Parent{
    public static int A = 1; 
    static{
        A = 2;
    }
}

static class Sub extends Parent{
    public static int B = A;
}

public static void main(String[] args){
    System.out.println(Sub.B);  //result: 2
}

虛擬機爲了保證一個類的<cinit>()方法在多線程環境中被正確地加鎖、同步。因而在多個線程同時去初始化一個類時,那麼只會有一個線程去執行這個類的<cinit>()方法,其餘線程都須要阻塞等待。 因而這裏就有個問題,若是一個類的<cinit>()方法有耗時很長的操做,就可能形成多個線程阻塞。

類加載器

重頭戲來了,瞭解上面的類加載過程以後,咱們對類加載有個感性的認識,因而咱們可使用類加載器去決定如何去獲取所需的類。
雖然類加載器僅僅實現類的加載動做(階段),但它在Java程序中起到的做用遠遠不限於類加載階段。
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性。
也就是說,判斷兩個類是否」相等「(這個「相等」包括類的Class對象的equals()方法、isAssignableForm()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字作對象所屬關係的斷定),只有在兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要它們的類加載器不同,那麼這兩個類就一定不一樣。

package com.jc.jvm.classloader;

import java.io.IOException;
import java.io.InputStream;

/**
 * 類加載器與instanceof關鍵字例子
 * 
 */
public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        //定義類加載器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".")+1)+".class";  // 只須要ClassLoaderTest.class
                InputStream in = getClass().getResourceAsStream(fileName);
                if(in==null){
                    return super.loadClass(name);
                }

                byte[] b = new byte[0];
                try {
                    b = new byte[in.available()];
                    in.read(b);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }

                return defineClass(name,b,0,b.length);


            }
        };


        //使用類加載器
        Object obj = myLoader.loadClass("com.jc.jvm.classloader.ClassLoaderTest").newInstance();



        //判斷class是否相同
        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.jc.jvm.classloader.ClassLoaderTest);
    }
}
/**output:
 * com.jc.jvm.classloader.ClassLoaderTest
 * false
 *
 */

雙親委派模型

大概瞭解類加載器是什麼東西以後。咱們來了解下,從JVM角度來看,有哪些類加載器。

從JVM的角度來說,只存在兩種不一樣的類加載器:

  1. 啓動類加載器(Bootstrap ClassLoader),這個類加載器是使用C++語言實現,是虛擬機自身的一部分。
  2. 另外一種就是其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且都繼承自抽象類java.lang.ClassLoader

而從Java開發人員的角度來看,類加載器還能夠劃分得跟細緻些:

  1. 啓動類加載器(Bootstrap ClassLoader): 這個類加載器負責將存放在$JAVA_HOME/lib目錄下的,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib目錄下也不會被加載)類庫加載到虛擬機內存中。能夠被-Xbootclasspath參數修改。啓動類加載器沒法被Java程序直接引用。
  2. 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Lancher$ExtClassLoader實現,負責加載$JAVA_HOME/lib/ext目錄下的,或者被java.ext.dirs系統變量指定的路徑中的全部類庫。開發者能夠直接使用擴展類加載器。
  3. 應用程序加載器(Application ClassLoader):這個類加載器由sum.misc.Launcher$AppClassLoader實現。因爲這個類加載器是ClassLoader的getSystemClassLoader()方法的返回值,因此通常也稱它爲 系統類加載器。若是應用程序中沒有自定義過本身的類加載器,則使用該類加載器做爲默認。它負責加載用戶類路徑(ClassPath)上所指定的類庫。

再加上自定義類加載器,那麼它們之間的層次關係爲:雙親委託模型(Parents Delegation Model)。雙親委託模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以集成繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來服用父加載器的代碼。

類加載的雙親委派模型實在JDK1.2期間引入的,但它不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。
雙親委派模型的工做過程是:若是一個類加載器收到類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此。所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。

雙親委派模型的實現:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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. 
                    c = findClass(name); 
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

雙親委派模型的破壞

因爲雙親委派模型不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。由於歷史緣由和需求不一樣因而出現過3次破壞:

第一次破壞

因爲java.lang.ClassLoader在JDK1.0就已經存在,而用戶去繼承ClassLoader,就是爲覆寫loadClass()方法,而這個方法實現有雙親委派模型的邏輯。因而這樣被覆蓋,雙親委派模型就被打破了。因而Java設計者在JDK1.2給ClassLoader添加一個新的方法findClass(),提倡你們應當把本身的類加載邏輯寫到findClass()方法中,這樣就不會破壞雙親委派模型的規則。由於loadClass()方法的邏輯裏就是若是父類加載失敗,則會調用本身的findClass()來完成加載,請看上面雙親委派模型的實現。

第二次破壞

雙親委派很好地解決了各個類加載器的基礎類的統一問題,但若是是基礎類,但啓動類加載器不認得怎麼辦。 如JNDI服務,JNDI在JDK1.3開始就做爲平臺服務,它的代碼是由啓動類加載器加載(JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它須要調用由獨立廠商實現並不熟在應用的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)代碼。但啓動類加載器不可能」認識「這些代碼。
因而Java設計團隊引入一個不太優雅的設計:就是線程上下文類加載器(Thread Context ClassLoader),這個類加載器能夠設置,但默認是就是應用程序類加載器。有了這個 線程上下文類加載器(這名字有點長) 後,就能夠作一些」舞弊「的事情(我喜歡稱爲hack),JNDI服務使用這個線程上下類加載器去加載所須要的SPI代碼,也就是父類加載器請求子類加載其去完成類加載的動做。 因而又一次違背了雙親委派模型。詳情請參考:javax.naming.InitialContext的源碼。這裏大概放出代碼:

//javax.naming.spi.NamingManager
public static Context getInitialContext(Hashtable<?,?> env)
        throws NamingException {
        InitialContextFactory factory;

        InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder();
        if (builder == null) {
            // No factory installed, use property
            // Get initial context factory class name

            String className = env != null ?
                (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;
            if (className == null) {
                NoInitialContextException ne = new NoInitialContextException(
                    "Need to specify class name in environment or system " +
                    "property, or as an applet parameter, or in an " +
                    "application resource file:  " +
                    Context.INITIAL_CONTEXT_FACTORY);
                throw ne;
            }

            try {
                factory = (InitialContextFactory)
                    helper.loadClass(className).newInstance(); //這個helper就是類加載器
            } catch(Exception e) {
                NoInitialContextException ne =
                    new NoInitialContextException(
                        "Cannot instantiate class: " + className);
                ne.setRootCause(e);
                throw ne;
            }
        } else {
            factory = builder.createInitialContextFactory(env);
        }

        return factory.getInitialContext(env);
    }
//獲取線程上下文類加載器

  ClassLoader getContextClassLoader() {

        return AccessController.doPrivileged(
            new PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                    ClassLoader loader =
                            Thread.currentThread().getContextClassLoader();  //線程類加載器
                    if (loader == null) {
                        // Don't use bootstrap class loader directly!
                        loader = ClassLoader.getSystemClassLoader();
                    }

                    return loader;
                }
            }
        );
    }
第三次破壞

此次破壞就嚴重咯,是因爲用戶對程序動態性的追求而致使的。也就是:代碼替換(HotSwap)、模塊熱部署(Hot Deployment)等。
對於模塊化之爭有,Sun公司的Jigsaw項目和OSGi組織的規範。

目前來看OSGi語句成爲了業界的Java模塊化標準。

OSGi實現模塊化熱部署的關鍵則是它的自定義類加載器機制的實現。每個程序模塊(OSGi中成爲Bundle)都有一個本身的類加載器。 當須要更換一個Bundle時,就把Bundle連同類加載器一塊兒換掉以實現代碼的熱替換。

在OSGi環境下,類加載器再也不是雙親委派模型中的樹狀結構,而是進一步發展爲更加複雜的網狀結構。
當收到類加載的請求時,OSGi將按照下面順序進行類搜索:

  1. 將以java.*開頭的類爲派給父類加載器架子啊
  2. 不然,將 委派列表名單內的類 委派給 父類加載器 加載
  3. 不然,將Import列表中的類 委派給Export這個類的Bundle的類加載器加載
  4. 不然,查找當前Bundle的ClassPath,使用本身的類加載器加載
  5. 不然,查找類是否在本身的Fragment Bundle中,若是在,則委派給Fragment Bundle的類加載器加載
  6. 不然,查找Dynamic Import列表的Bundle,委派給對應的Bundle的類加載器架子啊
  7. 不然,類查找失敗

總結

先大概瞭解類加載的過程
在瞭解類加載器是什麼東西
而後在瞭解雙親委派模型
最後實際就是爲熱部署作鋪墊,瞭解到都是爲需求而變化,並未強制使用某種規範。從3次雙親委派模型的破壞,咱們能夠看出這個模型並非很成熟。
OSGi中對類加載器的使用很值得學習,弄懂了OSGi的實現,就能夠算是掌握了類加載器的精髓。

參考《深刻理解Java虛擬機——JVM高級特性與最佳實踐》 周志明 機械工業出版社

相關文章
相關標籤/搜索