深入淺析Java類加載機(jī)制

類加載器簡(jiǎn)單來說是用來加載 Java 類到 Java 虛擬機(jī)中的。Java 虛擬機(jī)使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個(gè)實(shí)例。每個(gè)這樣的實(shí)例用來表示一個(gè) Java 類。通過此實(shí)例的 newInstance()方法就可以創(chuàng)建出該類的一個(gè)對(duì)象。

想要真正深入理解Java類加載機(jī)制,就要弄懂三個(gè)問題:類什么時(shí)候加載、類加載的過程是什么、用什么加載。所以本文分為三部分分別介紹Java類加載的時(shí)機(jī)、類加載的過程、加載器。

一、Java類加載的時(shí)機(jī)

1.1 類加載的生命周期
類加載的生命周期是從類被加載到內(nèi)存開始,直到卸載處內(nèi)存為止的。整個(gè)生命周期分為7個(gè)階段:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載。其中,驗(yàn)證、準(zhǔn)備、解析三部分統(tǒng)稱為連接。具體步驟如下圖所示:


下面簡(jiǎn)單介紹下類加載器所執(zhí)行的生命周期的過程。
(1) 裝載:查找和導(dǎo)入Class文件;

(2) 鏈接:把類的二進(jìn)制數(shù)據(jù)合并到JRE中;
(a)校驗(yàn):檢查載入Class文件數(shù)據(jù)的正確性;
(b)準(zhǔn)備:給類的靜態(tài)變量分配存儲(chǔ)空間;
(c)解析:將符號(hào)引用轉(zhuǎn)成直接引用;

(3) 初始化:對(duì)類的靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化操作。

1.2 類加載的時(shí)機(jī)
類加載的時(shí)機(jī)Java虛擬機(jī)規(guī)范中并沒有強(qiáng)制規(guī)定,但是對(duì)于初始化階段,有5種場(chǎng)景必須立即執(zhí)行初始化,也被稱為主動(dòng)引用。
(1) 遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候,讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。

(2) 使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。

(3) 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。

(4)當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。

(5)當(dāng)使用JDK 1.7動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且該方法句柄所對(duì)應(yīng)的類沒有初始化過,則先觸發(fā)初始化。

二、Java類加載的過程

類加載的全過程分為7個(gè)階段,但是主要的過程是加載、驗(yàn)證、準(zhǔn)備、解析、初始化這5個(gè)階段。
2.1 加載
在加載階段,虛擬機(jī)需要完成3件事情:
(1) 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流;

(2) 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);

(3) 在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。

2.2 驗(yàn)證
驗(yàn)證階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。整體來看,驗(yàn)證階段大致分為4個(gè)驗(yàn)證動(dòng)作。
(1)文件格式驗(yàn)證
第一階段是驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類型信息的要求。該階段是基于二進(jìn)制字節(jié)流驗(yàn)證的,只有通過了這個(gè)階段的驗(yàn)證,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法去中存儲(chǔ),后面的3個(gè)驗(yàn)證都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的。
這一階段可能的驗(yàn)證點(diǎn):

  • 是否以魔數(shù)開頭;
  • 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍內(nèi);
  • 常量池的常量數(shù)據(jù)類型是否被支持;
  • 。。。

(2)元數(shù)據(jù)驗(yàn)證
元數(shù)據(jù)驗(yàn)證是對(duì)字節(jié)碼描述信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求。這個(gè)階段可能的驗(yàn)證點(diǎn):

  • 是否有父類;
  • 是否繼承了不被允許繼承的類;
  • 如果該類不是抽象類,是否實(shí)現(xiàn)了其父類或接口要求實(shí)現(xiàn)的所有方法;
  • 。。。

(3)字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證的主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義的合法性和邏輯性。該階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事情。這個(gè)階段可能的驗(yàn)證點(diǎn):

  • 保證任何時(shí)候操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列的一致性;
  • 跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上;
  • 。。。

(4)符號(hào)引用驗(yàn)證
符號(hào)引用驗(yàn)證的主要目的是保證解析動(dòng)作能正常執(zhí)行,如果無法通過符號(hào)引用驗(yàn)證,則會(huì)拋出異常。這個(gè)階段可能的驗(yàn)證點(diǎn):

  • 符號(hào)引用的類、字段、方法的訪問性(public、private等)是否可被當(dāng)前類訪問;
  • 指定類是否存在符合方法的字段描述符;
  • 。。。

2.3 準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配,需要說明的是:
這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中;這里所說的初始值“通常情況”是數(shù)據(jù)類型的零值,例如:

public static int value = 1;

value在準(zhǔn)備階段過后的初始值為0而不是1,而把value賦值的putstatic指令將在初始化階段才會(huì)被執(zhí)行。

特殊情況:
public static final int value = 1;//此時(shí)準(zhǔn)備階段value賦值為1

2.4 解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換成直接引用的過程。直接引用是直接指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存有關(guān),同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用不盡相同。

2.5 初始化
初始化階段是類加載過程的最后一步,到了該階段才真正開始執(zhí)行類定義的Java程序代碼,根據(jù)程序員通過代碼定制的主觀計(jì)劃去初始化類變量和其他資源,是執(zhí)行類構(gòu)造器初始化方法的過程。

三、類加載器

類加載器大致可以分為以下3部分:
(1) 啟動(dòng)類加載器: 將存放于<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會(huì)被加載)類庫加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類加載器無法被Java程序直接引用。
(2) 擴(kuò)展類加載器 : 將<JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫加載。開發(fā)者可以直接使用擴(kuò)展類加載器。
(3) 應(yīng)用程序類加載器: 負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可直接使用。

我們的應(yīng)用程序都是由這三種類加載器相互配合加載的。它們的關(guān)系如下圖所示,稱之為雙親委派模型。

類加載器雙親委派模型

工作過程:如果一個(gè)類加載器接收到了類加載的請(qǐng)求,它首先把這個(gè)請(qǐng)求委托給他的父類加載器去完成,每個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求(它在搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。

好處:java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar中,無論哪個(gè)類加載器要加載這個(gè)類,最終都會(huì)委派給啟動(dòng)類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。相反,如果用戶自己寫了一個(gè)名為java.lang.Object的類,并放在程序的Classpath中,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類,java類型體系中最基礎(chǔ)的行為也無法保證,應(yīng)用程序也會(huì)變得一片混亂。

雙親委派模型實(shí)現(xiàn)起來其實(shí)很簡(jiǎn)單,以下是實(shí)現(xiàn)代碼,通過以下代碼,可以對(duì)JVM采用的雙親委派類加載機(jī)制有了更感性的認(rèn)識(shí)。

public Class<?> loadClass(String name)throws ClassNotFoundException {
        return loadClass(name, false);
}

protectedsynchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // 首先判斷該類型是否已經(jīng)被加載
        Class c = findLoadedClass(name);

        if (c == null) {
            //如果沒有被加載,就委托給父類加載或者委派給啟動(dòng)類加載器加載
            try {
                if (parent != null) {
                    //如果存在父類加載器,就委派給父類加載器加載
                    c = parent.loadClass(name, false);
                } else {
                  //如果不存在父類加載器,就檢查是否是由啟動(dòng)類加載器加載的類,通過調(diào)用本地方法
                  native Class findBootstrapClass(String name)
                  c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
              // 如果父類加載器和啟動(dòng)類加載器都不能完成加載任務(wù),才調(diào)用自身的加載功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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