JVM類加載機制理解

Github issues:https://github.com/littlejoyo/Blog/issues/

個人博客:https://littlejoyo.github.io/

微信公眾號:Joyo說

JVM的內(nèi)容是面試的重點內(nèi)容,常見的重點內(nèi)容集中于:類加載機制的理解以及JVM垃圾回收機制的理解,這里先介紹一下JVM的類加載機制,總結(jié)一下類加載機制的過程。

一、類加載過程

JVM類加載分為下面幾個個過程:加載,驗證,準備,解析,初始化,使用,卸載,如下圖所示:


類加載機制

主要是理解好加載、驗證、準備、解析、初始化這5個過程以及對應(yīng)的作用。

1.1加載

加載主要是將.class文件(并不一定是.class??梢允荶IP包,網(wǎng)絡(luò)中獲?。┲械亩M制字節(jié)流讀入到JVM中。
在加載階段,JVM需要完成3件事:
1)通過類的全限定名獲取該類的二進制字節(jié)流;
2)將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);
3)在內(nèi)存中生成一個該類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。

1.2連接

1.2.1 驗證

驗證是連接階段的第一步,主要確保加載進來的字節(jié)流符合JVM規(guī)范。
驗證階段會完成以下4個階段的檢驗動作:
1)文件格式驗證
2)元數(shù)據(jù)驗證(是否符合Java語言規(guī)范)
3)字節(jié)碼驗證(確定程序語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執(zhí)行)

1.2.2 準備

準備是連接階段的第二步,主要為靜態(tài)變量在方法區(qū)分配內(nèi)存,并設(shè)置默認初始值。

1.2.3 解析

解析是連接階段的第三步,是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。

1.3初始化

初始化階段是類加載過程的最后一步,主要是根據(jù)程序中的賦值語句主動為類變量賦值。
注:
1)當有父類且父類為初始化的時候,先去初始化父類;
2)再進行子類初始化語句。

什么時候需要對類進行初始化?
1)使用new該類實例化對象的時候;
2)讀取或設(shè)置類靜態(tài)字段的時候(但被final修飾的字段,在編譯器時就被放入常量池的靜態(tài)字段除外static final);
3)調(diào)用類靜態(tài)方法的時候;
4)使用反射Class.forName(“xxxx”)對類進行反射調(diào)用的時候,該類需要初始化;
5) 初始化一個類的時候,有父類,先初始化父類(注:1. 接口除外,父接口在調(diào)用的時候才會被初始化;2.子類引用父類靜態(tài)字段,只會引發(fā)父類初始化);
6) 被標明為啟動類的類(即包含main()方法的類)要初始化;

2.舉例驗證

2.1通過一道代碼實例題了解類加載過程

class Singleton1{
    private static Singleton1 singleton1 = new Singleton1();
    public static int value1;
    public static int value2 = 0;

    private Singleton1(){
        value1++;
        value2++;
    }

    public static Singleton1 getInstance(){
        return singleton;
    }

}

class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();

    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {
        Singleton1 singleton = Singleton1.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1);
        System.out.println("Singleton1 value2:" + singleton.value2);

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1);
        System.out.println("Singleton2 value2:" + singleton2.value2);
    }


上面兩個單例類主要的區(qū)別就是創(chuàng)建實例語句的放置位置的不同,這里會涉及到初始化操作對value值的影響。解析前先看輸出的結(jié)果是怎樣的?


輸出結(jié)果:
Singleton1 value1 : 1
Singleton1 value2 : 0
Singleton2 value1 : 1
Singleton2 value2 : 1

2.2解析

Singleton輸出結(jié)果:1 0

1 首先執(zhí)行main中的Singleton singleton = Singleton.getInstance();
2 類的加載:加載類Singleton
3 類的驗證
4 類的準備:為靜態(tài)變量分配內(nèi)存,設(shè)置默認值。這里為singleton(引用類型)設(shè)置為null,value1,value2(基本數(shù)據(jù)類型)設(shè)置默認值0
5 類的初始化(按照賦值語句進行修改):
執(zhí)行private static Singleton singleton = new Singleton();
執(zhí)行Singleton的構(gòu)造器:value1++;value2++; 此時value1,value2均等于1
執(zhí)行
public static int value1;
public static int value2 = 0;
此時value1=1,value2=0

Singleton2輸出結(jié)果:1 1

1 首先執(zhí)行main中的Singleton2 singleton2 = Singleton2.getInstance2();
2 類的加載:加載類Singleton2
3 類的驗證
4 類的準備:為靜態(tài)變量分配內(nèi)存,設(shè)置默認值。這里為value1,value2(基本數(shù)據(jù)類型)設(shè)置默認值0,singleton2(引用類型)設(shè)置為null,
5 類的初始化(按照賦值語句進行修改):
執(zhí)行
public static int value2 = 0;
此時value2=0(value1不變,依然是0);
執(zhí)行
private static Singleton singleton = new Singleton();
執(zhí)行Singleton2的構(gòu)造器:value1++;value2++;
此時value1,value2均等于1,即為最后結(jié)果

二、類加載器與雙親委派模型

一開始我一直很難理解為什么要叫做雙親委派,后來才發(fā)現(xiàn)這只是翻譯過來約定俗成的說法罷了,Parent翻譯成了雙親。

類加載器實現(xiàn)的功能是即為加載階段獲取二進制字節(jié)流的時候。
JVM提供了以下3種系統(tǒng)的類加載器:

  • 啟動類加載器(Bootstrap ClassLoader):最頂層的類加載器,負責(zé)加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
  • 擴展類加載器(Extension ClassLoader):負責(zé)加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。
  • 應(yīng)用程序類加載器(Application ClassLoader):也叫做系統(tǒng)類加載器,可以通過getSystemClassLoader()獲取,負責(zé)加載用戶路徑(classpath)上的類庫。如果沒有自定義類加載器,一般這個就是默認的類加載器。

類加載器之間的層次關(guān)系:


雙親委派模型

雙親委派模型要求除了頂層的啟動類加載器(Bootstrap ClassLoader)外,其余的類加載器都應(yīng)當有自己的父類加載器。這里的類加載器之間的父子關(guān)系一般不是以繼承關(guān)系實現(xiàn)的,而是用組合實現(xiàn)的。
底層的類加載器可以通過getSystemClassLoader()方法來獲取到父類的類加載器,但是最頂層無法執(zhí)行這個方法,因為啟動類加載器是由C++語言設(shè)計的,沒有提供該方法。

雙親委派模型的工作過程

如果一個類接受到類加載請求,他自己不會去加載這個請求,而是將這個類加載請求委派給父類加載器,這樣一層一層傳送,直到到達啟動類加載器(Bootstrap ClassLoader)。 只有當父類加載器無法加載這個請求時,子加載器才會嘗試自己去加載。

雙親委派模型的作用

防止不可信任的類被當做可信類,保證了程序的安全性。

雙親委派模型的代碼實現(xiàn)

雙親委派模型的代碼實現(xiàn)集中在java.lang.ClassLoader的loadClass()方法當中。
1)首先檢查類是否被加載,沒有則調(diào)用父類加載器的loadClass()方法;
2)若父類加載器為空,則默認使用啟動類加載器作為父加載器;
3)若父類加載失敗,拋出ClassNotFoundException 異常后,再調(diào)用自己的findClass() 方法。
loadClass源代碼如下:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先檢查類是否被加載
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 沒有則調(diào)用父類加載器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父類加載器為空,則默認使用啟動類加載器作為父加載器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父類加載失敗,拋出ClassNotFoundException 異常后
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再調(diào)用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}

findClass()用于寫類加載邏輯、loadClass()方法的邏輯里如果父類加載器加載失敗則會調(diào)用自己的findClass()方法完成加載,保證了雙親委派規(guī)則。
1、如果不想打破雙親委派模型,那么只需要重寫findClass方法即可
2、如果想打破雙親委派模型,那么就重寫整個loadClass方法

破壞雙親委派模型的情況

雙親委派模型很好的解決了各個類加載器加載基礎(chǔ)類的統(tǒng)一性問題。即越基礎(chǔ)的類由越上層的加載器進行加載。
若加載的基礎(chǔ)類中需要回調(diào)用戶代碼,而這時頂層的類加載器無法識別這些用戶代碼,怎么辦呢?這時就需要破壞雙親委派模型了。
下面介紹兩個例子來講解破壞雙親委派模型的過程。

  • JNDI破壞雙親委派模型
    JNDI是Java標準服務(wù),它的代碼由啟動類加載器去加載。但是JNDI需要回調(diào)獨立廠商實現(xiàn)的代碼,而類加載器無法識別這些回調(diào)代碼(SPI)。
    為了解決這個問題,引入了一個線程上下文類加載器。 可通過Thread.setContextClassLoader()設(shè)置。
    利用線程上下文類加載器去加載所需要的SPI代碼,即父類加載器請求子類加載器去完成類加載的過程,而破壞了雙親委派模型。

  • Spring破壞雙親委派模型
    Spring要對用戶程序進行組織和管理,而用戶程序一般放在WEB-INF目錄下,由WebAppClassLoader類加載器加載,而Spring由Common類加載器或Shared類加載器加載。
    那么Spring是如何訪問WEB-INF下的用戶程序呢?
    使用線程上下文類加載器。 Spring加載類所用的classLoader都是通過Thread.currentThread().getContextClassLoader()獲取的。當線程創(chuàng)建時會默認創(chuàng)建一個AppClassLoader類加載器(對應(yīng)Tomcat中的WebAppclassLoader類加載器): setContextClassLoader(AppClassLoader)。
    利用這個來加載用戶程序。即任何一個線程都可通過getContextClassLoader()獲取到WebAppclassLoader。

Tomcat類加載架構(gòu)

Tomcat目錄下有4組目錄:

  • /common目錄下:類庫可以被Tomcat和Web應(yīng)用程序共同使用;由 Common ClassLoader類加載器加載目錄下的類庫;
  • /server目錄:類庫只能被Tomcat可見;由 Catalina ClassLoader類加載器加載目錄下的類庫;
  • /shared目錄:類庫對所有Web應(yīng)用程序可見,但對Tomcat不可見;由 Shared ClassLoader類加載器加載目錄下的類庫;
  • /WebApp/WEB-INF目錄:僅僅對當前web應(yīng)用程序可見。由 WebApp ClassLoader類加載器加載目錄下的類庫;
  • 每一個JSP文件對應(yīng)一個JSP類加載器。

文章參考來源https://blog.csdn.net/noaman_wgs/article/details/74489549?

微信公眾號

掃一掃關(guān)注Joyo說公眾號,共同學(xué)習(xí)和研究開發(fā)技術(shù)。

關(guān)注我
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容