天天看點

三、深入了解JAVA虛拟機之類加載機制

一、什麼是類加載機制

虛拟機把描述類的資料從 Class 檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化, 最終形成可以被虛拟機直接使用的 Java 類型,這就是虛拟機的類加載機制。

在Java語言裡,類型的加載、連接配接和初始化過程都是在程式需運作期間完成的。Java 裡天生可以動态擴充的語言特性就是依賴運作期動态加載和動态連接配接這個特點實作的。

二、類加載的時機

類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,整個生命周期包括:加載(Loading) 、 驗證(Verification) 、 準備(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、 使用(Using) 和解除安裝(Unloading) 7 個階段。

其中驗證、準備、解析 3 個部分統稱為連接配接(Linking) 。

三、深入了解JAVA虛拟機之類加載機制

加載、驗證、準備、初始化和解除安裝這 5 個階段的順序是确定的,類的加載過程必須按照這種 順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開 始,這是為了支援 Java 語言的運作時綁定( 也稱為動态綁定或晚期綁定) 。

虛拟機規範則是嚴格規定了有且隻有 5 種情況必須立即對類進行“初始化”( 而加載、驗證、 準備自然需要在此之前開始) :

  • 遇到 new、 getstatic、 putstatic 或 invokestatic 這 4 條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。比如使用new關鍵字建立對象的時候,讀取或設定一個類的靜态字段(被final修飾、已在編譯期把結果放入常量池的除外)的時候,以及調用一個類的靜态方法的時候。
  • 使用 java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始 化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main() 方法的那個類),虛拟機會先初始化這個主類。
  • 當使用 JDK1.7 的動态語言支援時,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果 REF_getStatic、 REF_putStatic、 REF_invokeStatic 的方法句柄,并且 這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

除了以上幾種方式外,通過其他方式引用類都不會觸發初始化。

比如:

通過子類引用父類的靜态字段,不會導緻子類初始化

通過數組定義來引用類,不會觸發此類的初始化

//不會觸發SuperClass和SubClass的初始化
SuperClass[] sc = new SubClass[];
           

常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,是以不 會觸發定義常量類的初始化。

public class ConstClass {
    static{
        System.out.println("ConstClass init");
    }
    public static final String  CONST = "const";
}
public static void main(String[] args) {
        //不會觸發ConstClass的初始化
        System.out.println(ConstClass.CONST);        
    }
           

當一個類在初始化的時候,要求其父類全部都已經初始化過了,但是一個接口在初始化時, 并不要求其父類接口全部完成初始化,隻有在真正使用到父接口的時候( 如引用接口中定義 的常量) 才會初始化。

三、類加載過程

1、加載

在加載階段,jvm需要完成以下三件事:

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

加載階段完成後,虛拟機外部的二進制位元組流就按照虛拟機所需的格式存儲在方法區之中, 方法區怎麼存儲由JVM自己定義。然後在記憶體中執行個體化一個java.lang.Class類的對象( 并沒有 明确規定是在Java堆中,對于HotSpot虛拟機而言,Class對象比較特殊,它雖然是對象,但 是存放在方法區裡面) 。 加載階段和連接配接階段的部分内容( 如一部分位元組碼檔案格式驗證動作) 是交叉進行的,加載 階段尚未完成,連接配接階段可能已經開始。

2、驗證

驗證是連接配接階段的第一步,這一階段的目的是為了確定 Class 檔案的位元組流中包含的資訊符 合目前虛拟機的要求,并且不會危害虛拟機自身的安全。

驗證階段大緻上會完成下面 4 個階段的檢驗動作:

  1. 檔案格式驗證

    這一階段要驗證位元組流是否符合 Class 檔案格式的規範,并且能被目前版本的虛拟機處理。

  2. 中繼資料驗證

    這一階段是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合 Java 語言規範的要求。

  3. 位元組碼驗證

    這一階段的主要目的是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。

  4. 符号引用驗證

    這一階段的主要目的是確定解析動作能正常執行。其校驗發生在虛拟機将符号引用轉化為直接引用的時候,這個轉化動作将在連接配接的第三階段———解析階段中發生。

如果所運作的全部代碼(包括自己編寫的及第三方包中的代碼)都已經被反複使用和驗證過,那麼在實施階段就可以考慮使用-Xverify:none 參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間。

3、準備

準備階段是正式為類變量配置設定記憶體并設定類變量的初始值的階段,這些變量所用的記憶體都将在方法區中進行配置設定。

這個階段進行記憶體配置設定的僅包括類變量( 被 static 修飾的變量) ,而不包括執行個體變量,執行個體變量将會在對象執行個體化時随着對象一起配置設定在 Java 堆中。

這裡所說的初始值“通常情況”下是資料類型的零值。

//value在準備階段過後的初始值為0,而不是123,把value指派為123的動作将在初始化階段(方法中)才會執行。
public static int value=;
           

特殊情況:如果類字段的字段屬性表中存在 ConstantValue 屬性,那在準備階段變量 value 就會被初始化為 ConstantValue 屬性所指定的值。

假設上面類變量 value 的定義變為: public static final int value= 123;

編譯時 Javac 将會為 value 生成 ConstantValue 屬性,在準備階段虛拟機就會根據 ConstantValue 的設定将 value 指派為 123。

4、解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程。

符号引用:符号引用以一組符号來描述所引用的目标。符号引用可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可,符号引用和虛拟機的布局無關,引用的目标不一定已經加載到記憶體中。

直接引用:直接引用和虛拟機的布局是相關的,不同的虛拟機對于相同的符号引用所翻譯出來的直接引用一般是不同的。如果有了直接引用,那麼直接引用的目标一定被加載到了記憶體中。 其可以是直接指向目标的指針,相對偏移量或是一個能間接定位到目标的句柄。

解析動作主要針對類或接口、字段解析、類方法解析、接口方法解析、方法類型解析、方法 句柄解析和調用點限定符 7 類符号引用進行。

5、初始化

初始化階段是執行類構造器 方法的過程。

  1. 方法是由編譯器自動收集類中的所有類變量的指派動作和靜态語句塊( static 塊) 中的語句合并産生的,編譯器收集的順序是由語句在源檔案中出現的順序決定的,靜态語句塊隻能通路到定義在靜态語句塊之前的變量,定義在它之後的變量,在前面的靜态語句塊可以指派,但是不能通路。
    public class Init {
       static{
           i = ;//可以指派
           //System.out.println(i);//不能通路
       }
       static int i = ;
    
       public static void main(String[] args) {
           System.out.println(i);
       }
    }
    輸出結果:
    
               
  2. 虛拟機保證子類的 方法執行之前,父類的 方法已經執行完畢,是以在虛拟機中第一個被執行的方法的類肯定Object。
  3. 對于接口,不能使用static塊,但是可以有靜态變量的指派操作。子類接口的 方 法調用并不保證父接口的 方法被先調用,隻有用到父接口的靜态變量的時候, 父接口 方法才會被調用。接口的實作類在初始化時也一樣不會執行接口 的 方法。
  4. 虛拟機會保證一個類的 方法在多線程環境中被正确地加鎖、同步。如果一個線 程的 方法調用時間過長,就可能造成多個線程阻塞。
  5. ()方法對已類或借口不是必須的,如果一個類中沒有靜态語句塊,也沒有對變量的指派操作,那麼編譯器可以不為這個類生成方法。

四、類加載器

通過一個類的全限定名來擷取描述此類的二進制位元組流,實作這一功能的代碼子產品成為類加載器。

1、類與類加載器

對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立其在Java虛拟機中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。即比較兩個類是否”相等“,隻有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個 Class 檔案,被同一個虛拟機加載,隻要加載它們的類加載器不同,那這兩個類就必定不相等。

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".")+)+".class";
                InputStream is = getClass().getResourceAsStream(fileName);
                if(is == null){
                    return super.loadClass(name);
                }
                byte[] b = null;
                try {
                    b = new byte[is.available()];
                    is.read(b);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return defineClass(name,b,,b.length);
            }
        };
        Object o = myLoader.loadClass("com.bw.oom.classloader.ClassLoaderTest").newInstance();
        System.out.println(o.getClass());
        System.out.println(o instanceof com.bw.oom.classloader.ClassLoaderTest);
    }
}
運作結果:
class com.bw.oom.classloader.ClassLoaderTest
false
           

可以看出對象o确實是類com.bw.oom.classloader.ClassLoaderTest的對象,但是其和com.bw.oom.classloader.ClassLoaderTest做類型所屬關系的時候卻反會了false,這是因為虛拟機中存在了兩個ClassLoaderTest類,一個由系統應用程式類加載器加載,另一個由自定義的加載器加載的,雖然來自同一個Class檔案,但依然是兩個類。

可以加-XX:+TraceClassLoading參數檢視系統加載了哪些類

//可以看到系統加載了兩個ClassLoaderTest類
[Loaded com.bw.oom.classloader.ClassLoaderTest from __JVM_DefineClass__]
[Loaded com.bw.oom.classloader.ClassLoaderTest$ from __JVM_DefineClass__]
           

2、雙親委派模型

1、三種類加載器

  • 啟動類加載器( Bootstrap ClassLoader):這個類将器負責将存放在 JAVA_HOME\lib 目 錄中的,或者被-Xbootclasspath 參數所指定的路徑中的,并且是虛拟機識别的(僅按照檔案名識别,如 rt.jar, 名字不符合的類庫即使放在 lib 目錄中也不會被加載) 類庫加載到虛拟機記憶體中。啟動類加載器無法被程式直接引用,使用者在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用 null 代替即可。
  • 擴充類加載器( Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader實作,它負責加載 JAVA_ HOME\lib\ext 目錄中的, 或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫。
  • 應用程式類加載器( Application ClassLoader):由sun.misc.Launcher$AppClassLoader實作,由于這個類加載器是 ClassLoader中的 getSystemClassLoader() 方法的傳回值,是以一般也稱它為系統類加載器。它負責加載使用者類路徑(ClassPath) 上所指定的類庫。如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。

2、雙親委派模型

三、深入了解JAVA虛拟機之類加載機制

上圖所示的類加載器之間的層次關系,稱為類加載器的雙親委派模型(Parents Delegation Model),雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裡類加載器之間的父子關系一般不會以繼承(Inheritance) 的關系來實作,而是都使 用組合(Composition) 關系來複用父加載器的代碼。

3、雙親委派模型工作流程

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都應該先傳送到頂層的啟動類加載器,隻有當父加載器回報自己無法完成這個加載請求(在它的搜尋範圍内沒有找到這個類)時,子加載器才會去嘗試加載。

4、雙親委派模型好處

java類随着它的加載器一起具備了一種帶有優先級的層次關系。類 java.lang.Object,它存放在 rt.jar 之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,是以 Object 類在程式的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型.由各個類加載器自行去加載的話.如果使用者編寫了一個稱為“java.lang.Object”的類.并存放在程式的ClassPath中.那系統中将會出現多個不同的Object類,java類型體系中最基礎的行為也就無法保證,應用程式也将會一片混亂。

5、雙親委派模型的實作

//java.lang.ClassLoader#loadClass()
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) {
                    // 如果父類加載器抛出ClassNotFoundException,說明父類加載器不能加載請求
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //在父類加載器不能加載的時候再調用本身的findClass方法進行加載
                    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;
        }
    }