天天看點

JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

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

一、類加載的時機

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

JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

隻有5種情況必須立即對類進行初始化(而加載、驗證、準備自然需要在此之前開始):

1.遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字執行個體化對象的時候、讀取或設定一個類的靜态字段(被final修飾、已在編譯期把結果放入常量池的靜态字段除外)的時候,以及調用一個類的靜态方法的時候。

2.使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

4.當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛拟機會先初始化這個主類。

5.當使用JDK 1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
           

        對于這5種會觸發類進行初始化的場景,虛拟機規範中使用了一個很強烈的限定語:“有且隻有”,這5種場景中的行為稱為對一個類進行主動引用,除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。

1.執行個體驗證第3、第4條

父類

public class Parent {
    static {
        System.out.println("parent 初始化了...");
    }
}
           

子類

public class Child extends Parent {

    static {
        System.out.println("child 初始化...");
    }

    public static void main(String[] args) {

    }

}
           

輸出:

JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

解析:執行的類中包含了main()會初始化這個類(第4條),在初始化子類時,發現父類沒有被初始化,則先初始化父類(第3條。)

2.不會觸發初始化的案例

a. 案例:通過子類引用父類的靜态字段,子類不會被初始化

父類

public class Parent {

    static {
        System.out.println("parent 初始化了...");
    }

    public static int num = 10;
}
           

子類

public class Child extends Parent {

    static {
        System.out.println("child 初始化...");
    }

}
           

執行類

public class Main {

    public static void main(String[] args) {
        System.out.println(Child.num);
    }
}
           
JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

初始化了父類,但是子類沒有初始化。

b. 案例:通過數組定義來引用類

将上面執行類的代碼做一下修改,子類和父類不改動。

執行類

public class Main {

    public static void main(String[] args) {
        Child[] children = new Child[10];
    }
}
           
JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

沒有初始化子類和父類。

c. 案例:調用類的常量。

将子類和執行類進行修改,父類不改動。

子類

public class Child extends Parent {

    public static final int a = 20;

    static {
        System.out.println("child 初始化...");
    }

}
           

執行類

public class Main {

    public static void main(String[] args) {
        System.out.println(Child.a);
    }
}
           
JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

二、類加載的過程

1.類加載的過程---加載

“加載”是“類加載”(Class Loading)過程的一個階段,在加載階段,虛拟機需要完成以下3件事情:

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

虛拟機規範的這3點要求并不具體。是以虛拟機實作與具體應用的靈活度都是相當大的。

比如第一條:沒有指明二進制位元組流要從一個Class檔案中擷取,準确來說是根本沒有指明從哪裡擷取,怎麼擷取。例如:

1.從ZIP包中讀取(典型場景:JAR、EAR、WAR)。
2.從網絡中擷取(典型場景:Applet)。
3.運作時計算生成(典型場景:動态代理技術,*$Proxy)。
4.由其它檔案生成(典型場景:JSP應用)。
5.從資料庫讀取(典型場景:中間伺服器)。
           

        數組類的特殊性:數組本身不通過類加載器建立,它是由Java虛拟機直接建立的,但數組類與類加載器仍然有很密切的關系,因為數組類的元素類型最終是要靠類加載器去建立,數組建立過程如下:

1.如果數組的元件類型是引用類型,那就遞歸采用類加載來加載。
2.如果數組的元件類型不是引用類型,Java虛拟機會把數組标記為引導類加載器關聯。
3.數組類的可見性與它的元件類型的可見性一緻,如果元件類型不是引用類型,那數組類的可見性将預設為public。
           

類加載無須等到“首次使用”該類時才加載該類,Java虛拟機規範允許系統預先加載某些類。

        加載階段完成後,虛拟機外部的二進制位元組流就按照虛拟機所需的格式存儲在方法區之中,方法區中的資料存儲格式由虛拟機實作自行定義,虛拟機規範未規定次區域的具體資料結構。

然後在記憶體中執行個體化一個java.lang.Class類的對象存放在方法區裡面,作為程式通路方法區中的這些類型資料的外部接口。

       加載階段與連接配接階段的部分内容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,加載階段尚未完成,連接配接階段可能已經開始,但這些夾在階段之中進行的動作,仍然處于連接配接階段的内容,這兩個階段的開始時間仍然保持着固定的先後順序。

2.類加載的過程---驗證

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

i. 檔案格式驗證

第一階段要驗證位元組流是否符合Class檔案格式的規範,并且能被目前版本的虛拟機處理(基于二進制位元組流)。這一階段可能包括下面這些驗證點:

1.是否以魔數0xCAFEBABE開頭。
2.主、次版本号是否在目前虛拟機處理範圍之内。
3.常量池的常量中是否有不被支援的常量類型(檢查常量tag标志)。
4.指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
5.CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料。
6.Class檔案中各個部分及檔案本身是否有被删除的或附加的其他資訊。
……
           

這一階段的驗證主要目的是保證輸入的位元組流能正确地解析并存儲方法區之内,格式上符合描述一個Java類型資訊的要求。

ii. 中繼資料驗證

第二階段是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求(基于方法區的存儲結構)。這個階段可能包括的驗證點如下:

1.這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)。
2.這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
3.如果這個類不是抽象類,是否實作了其父類或接口之中要求實作的所有方法。
4.類中的字段、方法是否與父類産生沖突(例如覆寫了父類的final字段,或者出現不符合 規則的方法重載,例5.如方法參數都一緻,但傳回值類型卻不同等)。
……
           

       第二階段的驗證主要目的是對類的中繼資料資訊進行語義校驗,保證不存在不符合Java語言規範的中繼資料資訊。

iii. 位元組碼驗證

在第二階段對中繼資料資訊中的資料類型做完校驗後,這個階段将對類的方法體進行校驗分析,保證被校驗類的方法在運作時不會做出危害虛拟機安全的事件(基于方法區的存儲結構),例如:

1.保證任意時刻操作數棧的資料類型與指令代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作棧放置了一個int類型的資料,使用時卻按long類型來加載入本地變量表中。
2.保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。
3.保證方法體中的類型轉換是有效的,例如可以把一個子類對象指派給父類資料類型,這是安全的,但是把父類對象指派給子類資料類型,甚至把對象指派給與它毫無繼承關系、完全不相幹的一個資料類型,則是危險和不合法的。
……
           

第三階段是整個驗證過程中最複雜的一個階段,主要目的是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。

iv. 符号引用驗證

符号引用驗證可以看做是對類自身以外(常量池中的各種符号引用)的資訊進行比對性校驗,通常需要校驗下列内容:

1.符号引用中通過字元串描述的全限定名是否能找到對應的類。
2.在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
3.符号引用中的類、字段、方法的通路性(private、protected、public、default)是否可被目前類通路。
……
           

最後一個階段的校驗發生在虛拟機将符号引用轉化為直接引用的時候,這個轉化動作将 在連接配接的第三階段——解析階段中發生。

         符号引用驗證的目的是確定解析動作能正常執行,如果無法通過符号引用驗證,那麼将 會抛出一個java.lang.IncompatibleClassChangeError異常的子類,如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

3.類加載的過程---準備

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

public static int value = 1024;
           

那變量value在準備階段過後的初始值為0而不是1024,因為這時候尚未開始做執行任何Java方法,而把value指派為1024的putstatic指令是程式被編譯後,存放于類構造器<clinit>()方法之中,是以把value指派為1024的動作将在初始化階段才會執行。

所有基本資料類型的零值

JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

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

public static final int value = 1024;
           

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

4.類加載的過程---解析

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

符号引用:

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

直接引用:

       直接引用可以是直接指向目标的指針,相對偏移量或是一個能間接定義到目标的句柄,直接引用是和虛拟機記憶體布局相關,引用的布标必定在記憶體中存在。

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

分别對應于常量池的

CONSTANT_Class_info、

CONSTANT_Fieldref_info、

CONSTANT_Methodref_info、

CONSTANT_InterfaceMethodref_info、

CONSTANT_MethodType_info、

CONSTANT_MethodHandle_info、

CONSTANT_InvokeDynamic_info 7種常量類型

5.類加載的過程---初始化

       類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段可以自定義類加載器參與之外,其餘都是由虛拟機主導與控制,到了初始化階段,才真正開始執行類中定義的Java程式代碼(位元組碼)。

從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

       如果執行<clinit>()方法那條線程退出後,其它線程喚醒之後也不會再次進行<clinit>()方法。同一個類加載器下,一個類型隻會初始化一次。

三、類加載器

類加載器:通過一個類的全限定名來擷取描述此類的二進制位元組流。

1.類與類加載器

類加載器雖然隻用于實作類的加載動作,但它在Java程式中起到的作用卻遠遠不限于類加載階段。

比較兩個類是否相等,需要這兩個類是由同一個類加載器加載的前提之下才有意義。否則哪怕是同一個Class檔案,這兩個類一定不相等。

相等指的是包括Class對象的equals()方法,isAsssignableFrom()方法,isInstance()方法的傳回結果,也包括使用instanceof關鍵字做對象所屬關系判定等情況。
           
  • i. 自定義類加載器

  1. 定義一個類,繼承ClassLoader
  2. 重寫loadClass方法
  3. 執行個體化Class對象
  • ii. 自定義類加載器的優勢

  1. 類加載器是Java語言的一項創新,也是Java語言流行的重要原因之一,最初的設計是為了滿足Java Applet的需求而開發出來的。
  2. 高度的靈活性
  3. 通過自定義類加載器可以實作熱部署
  4. 代碼加密
  • iii. 自定義類加載器案例

package com.kevin.jvm.classloader;

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

/**
 * @author caonanqing
 * @version 1.0
 * @description     測試自定義類加載器
 *      1.定義一個類,繼承ClassLoader
 *      2.重寫loadClass方法
 *      3.執行個體化Class對象
 * @createDate 2019/8/2
 */
public class ClassLoaderDemo {

    public static void main(String[] args) throws Exception {

        // 自定義類加載器
        ClassLoader mycl = new ClassLoader() {

            /**
             *  加載目前包下的所有類,其他的類委派父類加載器
             * @param name  類的全限定名
             * @return
             * @throws ClassNotFoundException   找不到類的時候會抛出這個異常
             */
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                // com.kevin.jvm.classloader.ClassLoaderDemo
                String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream ins = getClass().getResourceAsStream(filename); // 加載
                if(ins == null) {   // 沒有找到,讓父類加載器來加載
                    return super.loadClass(name);
                }
                try {
                    byte[] buff = new byte[ins.available()];
                    ins.read(buff);
                    return defineClass(name,buff,0,buff.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };
        // 有main方法的類,被會應用程式加載器加載一次,加上我們自定義的加載器,是以會被加載兩次
        Object c = mycl.loadClass("com.kevin.jvm.classloader.ClassLoaderDemo").newInstance();
        System.out.println(c.getClass());
        // 其實c是被應用程式加載類所加載的,是以他們并不是被同一個類加載器加載的、
        System.out.println(c instanceof ClassLoaderDemo);

    }
}
           

2.雙親委派模型

從Java虛拟機的角度來講,隻存在兩種不同的類加載器:

1.一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實作,是虛拟機自身的一部分。
2.另一種就是所有其他的類加載器,這些類加載器都由Java語言實作,獨立于虛拟機外部,并且全都繼承自抽象類java.lang.ClassLoader。
           

從Java開發人員的角度來看,大部分Java程式會使用到以下3中系統提供的類加載器:

1.啟動類加載器(Bootstrap ClassLoader):加載lib下或被-Xbootclasspath路徑下的類。
2.擴充類加載器(Extension ClassLoader):加載lib/ext或java.ext.dirs系統變量所指定的路徑中的所有類庫。
3.應用程式類加載器(Application ClassLoader):ClassLoader負責,加載使用者路徑(ClassPath)上所指定的類庫,一般也成為系統類加載器,預設使用這個。
           

        一般我們的應用程式都是這3種類加載器互相配合進行加載的,如果有必要還可以加入我們自定義的類加載器。這些類加載器之間的關系,稱為類加載的雙親委派模型。

如圖所示:

JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。

這裡類加載器之間的父子關系一般不會以繼承的關系來實作,而是都使用組合關系來複用類加載器的代碼。

工作流程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是将這個請求委派給父類加載器。隻有父類加載器無法完成時子類加載器才會嘗試加載。

a.案例

package java.lang;

/**
 * @author caonanqing
 * @version 1.0
 * @description         測試雙親類加載器
 * @createDate 2019/8/2
 */
public class Object {

    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("Hello World");
    }
}
           
JVM虛拟機---(14)類加載機制一、類加載的時機二、類加載的過程三、類加載器

這個類其實并沒有被加載。

b.雙親類加載器的案例

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) {
                    //當父類的加載器不為空,則通過父類的loadClass來加載該類
                    c = parent.loadClass(name, false);
                } else {
                    //當父類的加載器為空,則調用啟動類加載器來加載該類
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父類的類加載器無法找到相應的類,則抛出異常
            }

            if (c == null) {
                //當父類加載器無法加載時,則調用findClass方法來加載該類
                long t1 = System.nanoTime();
                c = findClass(name); //使用者可通過覆寫該方法,來自定義類加載器

                //用于統計類加載器相關的資訊
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //對類進行link操作
            resolveClass(c);
        }
        return c;
    }
}
           

3.破壞雙親委派模型

雙親委派模型并不是強制性的限制模型,而是Java設計者推薦給開發者的類加載器實作方式。

        線程上下文類加載器(Thread Context ClassLoader),這個類加載器可以通過java.lang.Thread類的setContextClassLoaser()方法進行設定,如果建立線程時還沒設定,它将會從父線程中繼承一個,如果在應用程式的全局範圍内都沒有設定過的話,那這個類加載器預設就是應用程式類加載器。

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

1.将以java.*開頭的類委派給父類加載器加載。
2.否則,将委派清單名單内的類委派給父類加載器加載。
3.否則,将Import清單中的類委派給Export這個類的Bundle的類加載器加載。
4.否則,查找目前Bundle的ClassPath,使用自己的類加載器加載。
5.否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的 類加載器加載。
6.否則,查找Dynamic Import清單的Bundle,委派給對應Bundle的類加載器加載。
7.否則,類查找失敗。 上面的查找順序中隻有開頭兩點仍然符合雙親委派規則,其餘的類查找都是在平級的類 加載器中進行的。
           

繼續閱讀