自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

面試官:說說類加載的幾個(gè)階段

開發(fā) 前端
當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class文件時(shí),該Class所依賴的和引用的其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個(gè)類加載器來加載。

一、摘要

我們知道 Java 是先通過編譯器將.java類文件轉(zhuǎn)成.class字節(jié)碼文件,然后再通過虛擬機(jī)將.class字節(jié)碼文件加載到內(nèi)存中來實(shí)現(xiàn)應(yīng)用程序的運(yùn)行。

那么虛擬機(jī)是什么時(shí)候加載class文件?如何加載class文件?class文件進(jìn)入到虛擬機(jī)后發(fā)生了哪些變化?

今天我們就一起來了解一下,虛擬機(jī)是如何加載類文件的。

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

經(jīng)常有面試官問,“類什么時(shí)候加載”和“類什么時(shí)候初始化”,從內(nèi)容上來說,似乎都在問同一個(gè)問題:class文件是什么時(shí)候被虛擬機(jī)加載到內(nèi)存中,并進(jìn)入可以使用的狀態(tài)?

從虛擬機(jī)角度來說,加載和初始化是類的加載過程中的兩個(gè)階段。

對于“什么時(shí)候加載”,Java 虛擬機(jī)規(guī)范中并沒有約束,每個(gè)虛擬機(jī)實(shí)例都可以按自身需要來自由實(shí)現(xiàn)。但基本上都遵循類在進(jìn)行初始化之前,需要先進(jìn)行加載class文件。

對于“什么時(shí)候初始化”,Java 虛擬機(jī)規(guī)范有明確的規(guī)定,當(dāng)符合以下條件時(shí)(包括但不限),并且虛擬機(jī)在內(nèi)存中沒有找到對應(yīng)的類信息,必須對類進(jìn)行“初始化”操作:

  • 使用new實(shí)例化對象時(shí),讀取或者設(shè)置一個(gè)類的靜態(tài)字段或方法時(shí)
  • 反射調(diào)用時(shí),例如Class.forName("com.xxx.Test")
  • 初始化一個(gè)類的子類,會(huì)首先初始化子類的父類
  • Java 虛擬機(jī)啟動(dòng)時(shí)標(biāo)明的啟動(dòng)類,比如main方法所在的類
  • JDK8 之后,接口中存在default方法,這個(gè)接口的實(shí)現(xiàn)類初始化時(shí),接口會(huì)在它之前進(jìn)行初始化

類在初始化開始之前,需要先經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析這四個(gè)階段的操作。

下面我們一起來看看類的加載過程。

三、類的加載過程

當(dāng)一個(gè)類需要被加載到虛擬機(jī)中執(zhí)行時(shí),虛擬機(jī)會(huì)通過類加載器,將其.class文件中的字節(jié)碼信息在內(nèi)存中轉(zhuǎn)化成一個(gè)具體的java.lang.Class對象,以便被調(diào)用執(zhí)行。

類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存,整個(gè)生命周期包括七個(gè)階段:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載,可以用如下圖來簡要概括。

圖片圖片

其中類加載的過程,可以用三個(gè)步驟(五個(gè)階段)來簡要描述:加載 -> 連接(驗(yàn)證、準(zhǔn)備、解析)-> 初始化。(驗(yàn)證、準(zhǔn)備、解析這3個(gè)階段統(tǒng)稱為連接)

其次加載、驗(yàn)證、準(zhǔn)備和初始化這四個(gè)階段發(fā)生的順序是確定的,必須按照這種順序按部就班的開始,而解析階段則不一定。在某些情況下解析階段可以在初始化階段之后開始,這是為了支持 Java 語言的運(yùn)行時(shí)綁定,也稱為動(dòng)態(tài)綁定或晚期綁定。

同時(shí),這五個(gè)階段并不是嚴(yán)格意義上的按順序完成,在類加載的過程中,這些階段會(huì)互相混合,可能有些階段完成了,有些階段沒有完成,會(huì)交叉運(yùn)行,最終完成類的加載和初始化。

接下來依此分解一下加載、驗(yàn)證、準(zhǔn)備、解析、初始化這五個(gè)步驟,這五個(gè)步驟組成了一個(gè)完整的類加載過程。使用沒什么好說的,卸載通常屬于 GC 的工作,當(dāng)一個(gè)類沒有被任何地方引用并且類加載器已被 GC 回收,GC 會(huì)將當(dāng)前類進(jìn)行卸載,在后續(xù)的文章我們會(huì)介紹 GC 的工作機(jī)制。

3.1、加載

加載是類加載的過程的第一個(gè)階段,這個(gè)階段的主要工作是查找并加載類的二進(jìn)制數(shù)據(jù),在虛擬機(jī)中,類的加載有兩種觸發(fā)方式:

  • 預(yù)先加載:指的是虛擬機(jī)啟動(dòng)時(shí)加載,例如JAVA_HOME/lib/下的rt.jar下的.class文件,這個(gè)jar包里面包含了程序運(yùn)行時(shí)常用的文件內(nèi)容,例如java.lang.*、java.util.*、java.io.*等等,因此會(huì)隨著虛擬機(jī)啟動(dòng)時(shí)一起加載到內(nèi)存中。要證明這一點(diǎn)很簡單,自己可以寫一個(gè)空的main函數(shù),設(shè)置虛擬機(jī)參數(shù)為-XX:+TraceClassLoading,運(yùn)行程序就可以獲取類加載的全部信息
  • 運(yùn)行時(shí)加載:虛擬機(jī)在用到一個(gè).class文件的時(shí)候,會(huì)先去內(nèi)存中查看一下這個(gè).class文件有沒有被加載,如果沒有,就會(huì)按照類的全限定名來加載這個(gè)類;如果有,就不會(huì)加載。

無論是哪種觸發(fā)方式,虛擬機(jī)在加載.class文件時(shí),都會(huì)做以下三件事情:

  • 1.通過類的全限定名定位.class文件,并獲取其二進(jìn)制字節(jié)流
  • 2.將類信息、靜態(tài)變量、字節(jié)碼、常量這些.class文件中的內(nèi)容放入運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)中
  • 3.在內(nèi)存中生成一個(gè)代表這個(gè).class文件的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口,一般這個(gè)java.lang.Class對象會(huì)存在 Java 堆中

虛擬機(jī)規(guī)范對這三點(diǎn)的要求并不具體,因此具體虛擬機(jī)實(shí)現(xiàn)的靈活度都很大。比如第一條,沒有指明二進(jìn)制字節(jié)流要從哪里來,單單就這一條,就能變出許多花樣來,比如下面幾種加載方式:

  • 從 zip、jar、ear、war 等歸檔文件中加載.class文件
  • 通過網(wǎng)絡(luò)下載并加載.class文件,典型應(yīng)用就是 Applet
  • 將Java源文件動(dòng)態(tài)編譯為.class文件,典型應(yīng)用就是動(dòng)態(tài)代理技術(shù)
  • 從數(shù)據(jù)庫中提取.class文件并進(jìn)行加載

總的來說,加載階段(準(zhǔn)確地說,是加載階段獲取類的二進(jìn)制字節(jié)流的動(dòng)作)對于開發(fā)者來說是可控性最強(qiáng)的一個(gè)階段。因?yàn)殚_發(fā)者既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義類加載器來完成加載。

3.2、驗(yàn)證

驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保.class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。

Java 語言本身是比較安全的語言,但是正如上面說到的.class文件未必是從 Java 源碼編譯而來,可以使用任何途徑來生成并加載。虛擬機(jī)如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對自身保護(hù)的一項(xiàng)重要工作。

驗(yàn)證階段大致會(huì)完成 4 項(xiàng)檢驗(yàn)工作:

  • 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,例如:是否以0xCAFEBABE開頭、主次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型等
  • 元數(shù)據(jù)驗(yàn)證:對字節(jié)碼描述的元數(shù)據(jù)信息進(jìn)行語義分析,要符合 Java 語言規(guī)范,例如:是否繼承了不允許被繼承的類(例如 final 修飾過的)、類中的字段、方法是否和父類產(chǎn)生矛盾等等
  • 字節(jié)碼驗(yàn)證:對類的方法體進(jìn)行校驗(yàn)分析,確保這些方法在運(yùn)行時(shí)是合法的、符合邏輯的
  • 符號(hào)引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行,例如:確保符號(hào)引用的全限定名能找到對應(yīng)的類,符號(hào)引用中的類、字段、方法允許被當(dāng)前類所訪問等等

驗(yàn)證階段是非常重要的,但不是必須的,它對程序運(yùn)行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗(yàn)證,那么可以考慮采用-Xverify:none參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。

3.3、準(zhǔn)備

準(zhǔn)備是連接階段的第二步,這個(gè)階段的主要工作是正式為類變量分配內(nèi)存并設(shè)置其初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配。

不過這個(gè)階段,有幾個(gè)知識(shí)點(diǎn)需要注意一下:

  • 1.這時(shí)候進(jìn)行內(nèi)存分配的僅僅是類變量(被static修飾的變量),而不是實(shí)例變量,實(shí)例變量將會(huì)在對象實(shí)例化的時(shí)候隨著對象一起分配在 Java 堆中
  • 2.這個(gè)階段會(huì)設(shè)置變量的初始值,值為數(shù)據(jù)類型默認(rèn)的零值(如 0、0L、null、false 等),不是在代碼中被顯式地賦予的值;但是當(dāng)字段被final修飾時(shí),這個(gè)初始值就是代碼中顯式地賦予的值
  • 3.在 JDK1.8 取消永久代后,方法區(qū)變成了一個(gè)邏輯上的區(qū)域,這些類變量的內(nèi)存實(shí)際上是分配在 Java 堆中的,跟 JDK1.7 及以前的版本稍有不同

關(guān)于第二個(gè)知識(shí)點(diǎn),我們舉個(gè)簡單的例子進(jìn)行講解,比如public static int value = 123,value在準(zhǔn)備階段過后是0而不是123。

因?yàn)檫@時(shí)候尚未開始執(zhí)行任何 Java 方法,把value賦值為123的public static指令是在程序編譯后存放于類構(gòu)造器<clinit>()方法之中的,因此把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。

假如被final修飾,比如public static final int value = 123就不一樣了,編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段,虛擬機(jī)就會(huì)給value賦值為123,因?yàn)檫@個(gè)變量無法被修改,會(huì)存入類的常量池中。

各個(gè)數(shù)據(jù)類型的零值如下圖:

數(shù)據(jù)類型

零值

byte

0

short

0

int

0

long

0L

float

0.0f

double

0.0d

boolean

false

char

\u0000

reference

null

3.4、解析

解析是連接階段的第三步,這個(gè)階段的主要工作是虛擬機(jī)會(huì)把這個(gè).class文件中常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用。

主要解析的是類或接口、字段、方法等符號(hào)引用,我們可以把解析階段中符號(hào)引用轉(zhuǎn)換為直接引用的過程,理解為當(dāng)前加載的這個(gè)類和它所引用的類,正式進(jìn)行“連接“的過程。

我們先來了解一下符號(hào)引用和直接引用有什么區(qū)別:

  • 符號(hào)引用:這個(gè)其實(shí)是屬于編譯原理方面的概念,Java 代碼在編譯期間,是不知道最終引用的類型,具體指向內(nèi)存中哪個(gè)位置的,這時(shí)候會(huì)使用一個(gè)符號(hào)引用來表示具體引用的目標(biāo)是"誰",符號(hào)引用和虛擬機(jī)的內(nèi)存布局是沒有關(guān)系的
  • 直接引用:指的是可以直接或間接指向目標(biāo)內(nèi)存位置的指針或句柄,直接引用和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局是有關(guān)系的

符號(hào)引用轉(zhuǎn)換為直接引用,可以理解成將某個(gè)符號(hào)與虛擬機(jī)中的內(nèi)存位置建立連接,通過指針或句柄來直接訪問目標(biāo)。

與此同時(shí),同一個(gè)符號(hào)引用在不同的虛擬機(jī)實(shí)現(xiàn)上翻譯出來的直接引用一般不會(huì)相同。

3.5、初始化

初始化是類加載的過程的最后一步,這個(gè)階段的主要工作是執(zhí)行類構(gòu)造器 <clinit>()方法的過程。

簡單的說,初始化階段做的事就是給static變量賦予用戶指定的值,同時(shí)類中如果存在static代碼塊,也會(huì)執(zhí)行這個(gè)靜態(tài)代碼塊里面的代碼。

初始化階段,虛擬機(jī)大致依此會(huì)進(jìn)行如下幾個(gè)步驟的操作:

  • 1.檢查這個(gè)類是否被加載和連接,如果沒有,則程序先加載并連接該類
  • 2.檢查該類的直接父類有沒有被初始化,如果沒有,則先初始化其直接父類
  • 3.類中如果有多個(gè)初始化語句,比如多個(gè)static代碼塊,則依次執(zhí)行這些初始化語句

有個(gè)地方需要注意的是:虛擬機(jī)會(huì)保證類的初始化在多線程環(huán)境中被正確地加鎖、同步執(zhí)行,所以無需擔(dān)心是否會(huì)出現(xiàn)變量初始化時(shí)線程不安全的問題。

如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都會(huì)阻塞等待,直到<clinit>()方法執(zhí)行完畢。同時(shí),同一個(gè)類加載器下,一個(gè)類只會(huì)初始化一次,如果檢查到當(dāng)前類沒有初始化,執(zhí)行初始化;反之,不會(huì)執(zhí)行初始化。

與此同時(shí),只有當(dāng)對類的主動(dòng)使用的時(shí)候才會(huì)觸發(fā)類的初始化,觸發(fā)時(shí)機(jī)主要有以下幾種場景:

  • 1.創(chuàng)建類的實(shí)例對象,比如new一個(gè)對象操作
  • 2.訪問某個(gè)類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
  • 3.調(diào)用類的靜態(tài)方法
  • 4.反射操作,比如Class.forName("xxx")
  • 5.初始化某個(gè)類的子類,則其父類也會(huì)被初始化,并且父類具有優(yōu)先被初始化的優(yōu)勢
  • 6.Java 虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類,比如SpringBootApplication入口類

最后,<clinit>()方法和<init>()方法是不同的,一個(gè)是類構(gòu)造器初始化,一個(gè)是實(shí)例構(gòu)造器初始化,千萬別搞混淆了啊。

3.6、小結(jié)

當(dāng)一個(gè)符合 Java 虛擬機(jī)規(guī)范的.class字節(jié)碼文件,經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析、初始化這些 5 個(gè)階段相互協(xié)作執(zhí)行完成之后,虛擬機(jī)會(huì)將此文件的二進(jìn)制數(shù)據(jù)導(dǎo)入運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆內(nèi)存中,創(chuàng)建一個(gè)java.lang.Class類的對象,這個(gè)對象描述了這個(gè)類所有的信息,同時(shí)提供了這個(gè)類在方法區(qū)的訪問入口。

可以用如下圖來簡要描述。

圖片圖片

與此同時(shí),在方法區(qū)中,使用同一加載器的情況下,每個(gè)類只會(huì)有一份Class字節(jié)流信息;在堆內(nèi)存中,使用同一加載器的情況下,每個(gè)類也只會(huì)有一份java.lang.Class類的對象。

四、類加載器

在上文類的加載過程中,我們有提到在加載階段,通過一個(gè)類的全限定名來獲取此類的二進(jìn)制字節(jié)流操作,其實(shí)類加載器就是用來實(shí)現(xiàn)這個(gè)操作的。

在虛擬機(jī)中,任何一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間,對于類也同樣如此。

簡單的說,在虛擬機(jī)中看兩個(gè)類是否相同,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則即使這兩個(gè)類來源于同一個(gè).class文件,被同一個(gè)虛擬機(jī)加載,但是它們的類加載器不同,這兩個(gè)類必定不相等。

當(dāng)年為了滿足瀏覽器上 Java Applet 的需求,Java 的開發(fā)團(tuán)隊(duì)設(shè)計(jì)了類加載器,它獨(dú)立于 Java 虛擬機(jī)外部,同時(shí)也允許用戶按自身需要自行實(shí)現(xiàn)類加載器。通過類加載器,可以讓同一個(gè)類可以實(shí)現(xiàn)訪問隔離、OSGi、程序熱部署等等場景。發(fā)展至今,類加載器已經(jīng)是 Java 技術(shù)體系的一塊重要基石。

4.1、類加載器介紹

如果要查找類加載器,通過Thread.currentThread().getContextClassLoader()方法可以獲取。

簡單示例如下:

public class ClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println("current loader:" +  loader);
        System.out.println("parent loader:" +  loader.getParent());
        System.out.println("parent parent loader:" +  loader.getParent().getParent());
    }
}

輸出結(jié)果如下:

current loader:sun.misc.Launcher$AppClassLoader@18b4aac2
parent loader:sun.misc.Launcher$ExtClassLoader@511d50c0
parent parent loader:null

從運(yùn)行結(jié)果可以看到,當(dāng)前的類加載器是AppClassLoader,它的上一級是ExtClassLoader,再上一級是null。

其實(shí)ExtClassLoader的上一級是有類加載器的,它叫Bootstrap ClassLoader,是一個(gè)啟動(dòng)類加載器,由 C++ 實(shí)現(xiàn),不是 ClassLoader 子類,因此以 null 作為結(jié)果返回。

這幾種類加載器的層次關(guān)系,可以用如下圖來描述。

圖片圖片

它們之間的啟動(dòng)流程,可以通過以下內(nèi)容來簡單描述:

  • 1.在虛擬機(jī)啟動(dòng)后,會(huì)優(yōu)先初始化Bootstrap Classloader
  • 2.接著Bootstrap Classloader負(fù)責(zé)加載ExtClassLoader,并且將 ExtClassLoader的父加載器設(shè)置為Bootstrap Classloader
  • 3Bootstrap Classloader加載完ExtClassLoader后,就會(huì)加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader

因此,在加載 Java 應(yīng)用程序中的class文件時(shí),這里的父類加載器并不是通過繼承關(guān)系來實(shí)現(xiàn)的,而是互相配合進(jìn)行加載。

站在虛擬機(jī)的角度,只存在兩種不同的類加載器:

  • 啟動(dòng)類加載器:它由 C++ 實(shí)現(xiàn)(這里僅限于 Hotspot,不同的虛擬機(jī)可能實(shí)現(xiàn)不太一樣),是虛擬機(jī)自身的一部分
  • 其它類加載器:這些類加載器都由 Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī)之外,并且全部繼承自抽象類java.lang.ClassLoader,比如ExtClassLoader、AppClassLoader等,這些類加載器需要由啟動(dòng)類加載器加載到內(nèi)存中之后才能去加載其他的類

站在開發(fā)者的角度,類加載器大致可以劃分為三類:

  • 啟動(dòng)類加載器:比如Bootstrap ClassLoader,負(fù)責(zé)加載<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)制定的路徑,例如jre/lib/rt.jar里所有的class文件。同時(shí),啟動(dòng)類加載器是無法被 Java 程序直接引用的
  • 拓展類加載器:比如Extension ClassLoader,負(fù)責(zé)加載 Java 平臺(tái)中擴(kuò)展功能的一些 jar 包,包括<JAVA_HOME>\lib\ext目錄中或java.ext.dirs指定目錄下的 jar 包。同時(shí),開發(fā)者可以直接使用擴(kuò)展類加載器
  • 應(yīng)用程序類加載器:比如Application ClassLoader,負(fù)責(zé)加載ClassPath路徑下所有 jar 包,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下它就是程序中默認(rèn)的類加載器

當(dāng)然,如果有必要,也可以自定義類加載器,因?yàn)?JVM 自帶的 ClassLoader 只懂得從本地文件系統(tǒng)中加載標(biāo)準(zhǔn)的class文件,如果要從特定的場所取得class文件,例如數(shù)據(jù)庫中和網(wǎng)絡(luò)中,此時(shí)可以自己編寫對應(yīng)的 ClassLoader 類加載器。

4.2、雙親委派模型

在上文中我們提到,在虛擬機(jī)中,任何一個(gè)類由加載它的類加載器和這個(gè)類一同來確立其唯一性。

也就是說,JVM 對類的唯一標(biāo)識(shí),可以簡單的理解為由ClassLoader id + PackageName + ClassName組成,因此在一個(gè)運(yùn)行程序中有可能存在兩個(gè)包名和類名完全一致的類,但是如果這兩個(gè)類不是由一個(gè) ClassLoader 加載,會(huì)被視為兩個(gè)不同的類,此時(shí)就無法將一個(gè)類的實(shí)例強(qiáng)轉(zhuǎn)為另外一個(gè)類,這就是類加載器的隔離性。

為了解決類加載器的隔離問題,JVM 引入了雙親委派模型。

雙親委派模式,可以用一句話來說表達(dá):任何一個(gè)類加載器在接到一個(gè)類的加載請求時(shí),都會(huì)先讓其父類進(jìn)行加載,只有父類無法加載(或者沒有父類)的情況下,才嘗試自己加載。

大致流程圖如下:

圖片圖片

使用雙親委派模式,可以保證,每一個(gè)類只會(huì)有一個(gè)類加載器。例如 Java 最基礎(chǔ)的 Object 類,它存放在 rt.jar 之中,這是 Bootstrap 的職責(zé)范圍,當(dāng)向上委派到 Bootstrap 時(shí)就會(huì)被加載。

但如果沒有使用雙親委派模式,可以任由自定義加載器進(jìn)行加載的話,Java 這些核心類的 API 就會(huì)被隨意篡改,無法做到一致性加載效果。

JDK 中ClassLoader.loadClass()類加載器中的加載類的方法,部分核心源碼如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    // 1.首先要保證線程安全
    synchronized (getClassLoadingLock(name)) {
        // 2.先判斷這個(gè)類是否被加載過,如果加載過,直接跳過
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 3.有父類,優(yōu)先交給父類嘗試加載;如果為空,使用BootstrapClassLoader類加載器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父類加載失敗,這里捕獲異常,但不需要做任何處理
            }

            // 4.沒有父類,或者父類無法加載,嘗試自己加載
            if (c == null) {
                long t1 = System.nanoTime();
                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;
    }
}

4.3、自定義類加載器

在上文中我們提及過,針對某些特定場景,比如通過網(wǎng)絡(luò)來傳輸 Java 類的字節(jié)碼文件,為保證安全性,這些字節(jié)碼經(jīng)過了加密處理,這時(shí)系統(tǒng)提供的類加載器就無法對其進(jìn)行加載,此時(shí)我們可以自定義一個(gè)類加載器來完成文件的加載。

自定義類加載器也需要繼承ClassLoader類,簡單示例如下:

public class CustomClassLoader extends ClassLoader {

    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            byte[] data = loadClassData(name);
            if (data == null) {
                throw new ClassNotFoundException();
            }
            return defineClass(name, data, 0, data.length);
        }
        return null;
    }

    protected byte[] loadClassData(String name) {
        try {
            // package -> file folder
            name = name.replace(".", "http://");
            FileInputStream fis = new FileInputStream(new File(classPath + "http://" + name + ".class"));
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int len = -1;
            byte[] b = new byte[2048];
            while ((len = fis.read(b)) != -1) {
                baos.write(b, 0, len);
            }
            fis.close();
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

相關(guān)的測試類如下:

package com.example;

public class ClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println("current loader:" +  loader);
    }
}

將ClassLoaderTest.java源文件放在指定目錄下,并通過javac命令編譯成ClassLoaderTest.class,最后進(jìn)行測試。

public class CustomClassLoaderTest {

    public static void main(String[] args) throws Exception {
        String classPath = "/Downloads";
        CustomClassLoader customClassLoader = new CustomClassLoader(classPath);
        Class<?> testClass = customClassLoader.loadClass("com.example.ClassLoaderTest");
        Object obj = testClass.newInstance();
        System.out.println(obj.getClass().getClassLoader());
    }
}

輸出結(jié)果:

com.example.CustomClassLoader@60e53b93

在實(shí)際使用過程中,最好不要重寫loadClass方法,避免破壞雙親委派模型。

4.4、加載類的幾種方式

在類加載器中,有三種方式可以實(shí)現(xiàn)類的加載。

  • 1.通過命令行啟動(dòng)應(yīng)用時(shí)由 JVM 初始化加載,在上文已提及過
  • 2.通過Class.forName()方法動(dòng)態(tài)加載
  • 3.通過ClassLoader.loadClass()方法動(dòng)態(tài)加載

其中Class.forName()和ClassLoader.loadClass()加載方法,稍有區(qū)別:

  • Class.forName():表示將類的.class文件加載到 JVM 中之后,還會(huì)對類進(jìn)行解釋,執(zhí)行類中的static方法塊;
  • Class.forName(name, initialize, loader):支持通過參數(shù)來控制是否執(zhí)行類中的static方法塊;
  • ClassLoader.loadClass():它只將類的.class文件加載到 JVM,但是不執(zhí)行類中的static方法塊,只有在newInstance()才會(huì)去執(zhí)行static方法塊;

我們可以看一個(gè)簡單的例子!

public class ClassTest {

    static {
        System.out.println("初始化靜態(tài)代碼塊!");
    }
}
public class CustomClassLoaderTest {

    public static void main(String[] args) throws Exception {
        // 獲取當(dāng)前系統(tǒng)類加載器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // 1.使用Class.forName()來加載類,默認(rèn)會(huì)執(zhí)行初始化靜態(tài)代碼塊
        Class.forName(ClassTest.class.getName());

        // 2.使用Class.forName()來加載類,指定false,不會(huì)執(zhí)行初始化靜態(tài)代碼塊
//        Class.forName(ClassTest.class.getName(), false, classLoader);

        // 3.使用ClassLoader.loadClass()來加載類,不會(huì)執(zhí)行初始化靜態(tài)代碼塊
//        classLoader.loadClass(ClassTest.class.getName());
    }
}

運(yùn)行結(jié)果如下:

初始化靜態(tài)代碼塊!

切換不同的加載方式,會(huì)有不同的輸出結(jié)果!

4.5、小結(jié)

從以上的介紹中,針對類加載器的機(jī)制,我們可以總結(jié)出以下幾點(diǎn):

  • 全盤負(fù)責(zé):當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class文件時(shí),該Class所依賴的和引用的其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個(gè)類加載器來加載
  • 雙親委派:在接受類加載請求時(shí),會(huì)讓父類加載器試圖加載該類,只有在父類加載器無法加載該類或者沒有父類時(shí),才嘗試從自己的類路徑中加載該類
  • 按需加載:用戶創(chuàng)建的類,通常加載是按需進(jìn)行的,只有使用了才會(huì)被類加載器加載
  • 緩存機(jī)制:有被加載過的Class文件都會(huì)被緩存,當(dāng)要使用某個(gè)Class時(shí),會(huì)先去緩存查找,如果緩存中沒有才會(huì)讀取Class文件進(jìn)行加載。這就是為什么修改了Class文件后,必須重啟 JVM,程序的修改才會(huì)生效的原因

五、小結(jié)

本文從類的加載過程到類加載器,做了一次知識(shí)內(nèi)容講解,內(nèi)容比較多,如果有描述不對的地方,歡迎大家留言指出,不勝感激!

六、參考

1.https://zhuanlan.zhihu.com/p/25228545

2.http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html

3.https://www.cnblogs.com/xrq730/p/4844915.html

4.https://www.cnblogs.com/xrq730/p/4845144.html

責(zé)任編輯:武曉燕 來源: Java極客技術(shù)
相關(guān)推薦

2024-11-19 15:13:02

2025-04-16 00:00:01

JWT客戶端存儲(chǔ)加密令

2025-04-08 00:00:00

@AsyncSpring異步

2023-12-27 18:16:39

MVCC隔離級別幻讀

2024-05-30 08:04:20

Netty核心組件架構(gòu)

2024-08-22 10:39:50

@Async注解代理

2024-03-05 10:33:39

AOPSpring編程

2024-09-20 08:36:43

零拷貝數(shù)據(jù)傳輸DMA

2024-03-22 06:56:24

零拷貝技術(shù)數(shù)據(jù)傳輸數(shù)據(jù)拷貝

2020-07-02 07:52:11

RedisHash映射

2024-03-11 18:18:58

項(xiàng)目Spring線程池

2021-08-09 07:47:40

Git面試版本

2024-03-14 14:56:22

反射Java數(shù)據(jù)庫連接

2024-07-31 08:28:37

DMAIOMMap

2021-11-25 10:18:42

RESTfulJava互聯(lián)網(wǎng)

2024-12-06 07:00:00

2024-08-29 16:30:27

2024-08-12 17:36:54

2024-02-29 16:49:20

volatileJava并發(fā)編程

2021-07-28 10:08:19

類加載代碼塊面試
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)