還不知道Java類加載機制,你算白學(xué)了
1?前言
在Java的世界里,每一個類或者接口,在經(jīng)歷編譯器后,都會生成一個個.class文件。類加載機制指的是將這些.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,并對數(shù)據(jù)進(jìn)行校驗,解析和初始化。最終,每一個類都會在方法區(qū)保存一份它的元數(shù)據(jù),在堆中創(chuàng)建一個與之對應(yīng)的Class對象。類的生命周期,經(jīng)歷7個階段,分別是加載、驗證、準(zhǔn)備、解析、初始化、使用、卸載。
除了使用和卸載兩個過程,前面的5個階段?加載、驗證、準(zhǔn)備、解析、初始化?的執(zhí)行過程,就是類的加載過程。
2?類加載的時機
很多數(shù)人在問?“類什么時候加載” 和 “類什么時候初始化” ,從語境上來說,都是在問同一個問題,就是這個.class文件什么時候被讀取到虛擬機的內(nèi)存中,并且達(dá)到可用的狀態(tài)。
但嚴(yán)格意義上來說,加載和初始化,是類生命周期的兩個階段。對于什么時候加載,Java虛擬機規(guī)范中并沒有約束,各個虛擬機都可以按自身需要來自由實現(xiàn)。但絕大多數(shù)情況下,都遵循“什么時候初始化”來進(jìn)行加載。
什么時候初始化?Java虛擬機規(guī)范有明確規(guī)定,當(dāng)符合以下條件時(包括但不限于),虛擬機內(nèi)存中沒有找到對應(yīng)類型信息,則必須對類進(jìn)行“初始化”操作:
1.使用new實例化對象時、讀取或者設(shè)置一個類的靜態(tài)字段或方法時
2.反射調(diào)用時,例如 Class.forName("com.xxx.TestDemo")
3.初始化一個類的子類,會首先初始化子類的父類
4.Java虛擬機啟動時標(biāo)明的啟動類
5.JDK8 之后,接口中存在default方法,這個接口的實現(xiàn)類初始化時,接口會其之前進(jìn)行初始化
初始化階段開始之前,自然還是要先經(jīng)歷加載、驗證、準(zhǔn)備 、解析的。
3 類的加載過程
從前言中,我們知道,類的加載過程分?5 個階段,其中驗證、準(zhǔn)備、解析?可以歸納為 “連接” 階段。

需要注意的是,這5個階段,并不是嚴(yán)格意義上的按順序完成,在類加載的過程中,這些階段會互相混合,交叉運行,最終完成類的加載和初始化。?
例如在加載階段,需要使用驗證的能力去校驗字節(jié)碼正確性。在解析階段,也要使用驗證的能力去校驗符號引用的正確性。或者加載階段生成Class對象的時候,需要解析階段符號引用轉(zhuǎn)直接引用的能力等等......
接下來,我們詳細(xì)分解一下,這5個階段,都做了什么事情。

1.加載
加載是類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:
?通過一個類的全限定名去找到其對應(yīng)的.class文件
?將這個.class文件內(nèi)的二進(jìn)制數(shù)據(jù)讀取出來,轉(zhuǎn)化成方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
?在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口
Java虛擬機并沒有規(guī)定類的字節(jié)流必從.class文件中加載,在加載階段,程序員可以通過自定義的類加載器,自行定義讀取的地方,例如通過網(wǎng)絡(luò)、數(shù)據(jù)庫等。
2.驗證
Class文件中的內(nèi)容是字節(jié)碼,這些內(nèi)容可以由任何途徑產(chǎn)出,驗證階段的目的是保證文件內(nèi)容里的字節(jié)流符合Java虛擬機規(guī)范,且這些內(nèi)容信息運行后不會危害虛擬機自身的安全。
驗證階段會完成以下校驗:
1.文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范。例如:是否以0xCAFEBABE開頭、主次版本號是否在當(dāng)前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型等等
2.元數(shù)據(jù)驗證:對字節(jié)碼描述的元數(shù)據(jù)信息進(jìn)行語義分析,要符合Java語言規(guī)范。例如:是否繼承了不允許被繼承的類(例如final修飾過的)、類中的字段、方法是否和父類產(chǎn)生矛盾
字節(jié)碼驗證:對類的方法體進(jìn)行校驗分析,確保這些方法在運行時是合法的、符合邏輯的。
符號引用驗證:發(fā)生在解析階段,符號引用轉(zhuǎn)為直接引用的時候,例如:確保符號引用的全限定名能找到對應(yīng)的類、符號引用中的類、字段、方法允許被當(dāng)前類所訪問。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗證,那么可以考慮采用-Xverifynone參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
驗證階段不是必須的,雖然這個階段非常重要。Java虛擬機允許程序員主動取消這個階段,用來縮短類加載的時間,可以根據(jù)自身需求,使用 -Xverify:none參數(shù)來關(guān)閉大部分的類驗證措施。
3.準(zhǔn)備
這個階段,類的靜態(tài)字段信息(即使用?static 修飾過的變量)會得到內(nèi)存分配,并且設(shè)置為初始值。
對于該階段有以下幾個知識點需要注意:
1、內(nèi)存分配僅包括 static 修飾過的變量,而不包括實例變量,實例變量得等到對象實例化時分配內(nèi)存。
2、初始值指的是變量數(shù)據(jù)類型的默認(rèn)值,而不是被在Java代碼中被顯式地賦予的值。但是,當(dāng)字段信息被 final 修飾成常量(ConstantValue)時,這個初始值就是Java代碼中顯式地賦予的值。
例如:public static int value = 3
類變量 value 在準(zhǔn)備階段設(shè)置的初始值是 0,不是 3。把value賦值為3的 putstatic 指令是在程序編譯后,存放于類構(gòu)造器 <clinit>() 方法中的,所以把 value 賦值為 3 的動作將在初始化階段才會執(zhí)行。
當(dāng)使用 final 修飾后:public static final int value = 3
類變量 value 在準(zhǔn)備階段設(shè)置的初始值是 3,不是 0。
3、在JDK8取消永久代后,方法區(qū)變成了一個邏輯上的區(qū)域,這些類變量的內(nèi)存實際上是分配在Java堆中的。
4.解析
這個階段,虛擬機會把這個Class文件中,常量池內(nèi)的符號引用轉(zhuǎn)換為直接引用。主要解析的是 類或接口、字段、類方法、接口方法、方法類型、方法句柄等符號引用。我們可以把解析階段中,符號引用轉(zhuǎn)換為直接引用的過程,理解為當(dāng)前加載的這個類,和它所引用的類,正式進(jìn)行“連接“的過程。對于這段描述,很多人會問出下面幾個問題。
什么是符號引用?
Java代碼在編譯期間,是不知道最終引用的類型,具體指向內(nèi)存中哪個位置的,這時候會用一個符號引用,來表示具體引用的目標(biāo)是"誰"。Java虛擬機規(guī)范中明確定義了符號引用的形式,符合這個規(guī)范的前提下,符號引用可以是任意值,只要能通過這個值能定位到目標(biāo)。
什么是直接引用?
直接引用就是可以直接或間接指向目標(biāo)內(nèi)存位置的指針或句柄。
引用的類型,還未加載初始化怎么辦?
當(dāng)出現(xiàn)這種情況,會觸發(fā)這個引用對應(yīng)類型的加載和初始化。

5.初始化
這步類加載的最后一個步驟了,初始化的過程,就是執(zhí)行類構(gòu)造器 <clinit>()方法的過程。
當(dāng)初始化完成之后,類中static修飾的變量會賦予程序員實際定義的“值”,同時類中如果存在static代碼塊,也會執(zhí)行這個靜態(tài)代碼塊里面的代碼。
<clinit>() 方法的作用是什么?
還記得么?在準(zhǔn)備階段,已經(jīng)對類中static修飾的變量賦予了初始值。<clinit>() 方法的作用,就是給這些變量賦予程序員實際定義的“值”。同時類中如果存在static代碼塊,也會執(zhí)行這個靜態(tài)代碼塊里面的代碼。
<clinit>() 方法是什么?
<clinit>() 方法 和 <init> 方法是不同的,它們一個是“類構(gòu)造器”,一個是實例構(gòu)造器。Java虛擬機會保證子類<clinit>() 方法在執(zhí)行前,父類的 <clinit>() 已經(jīng)執(zhí)行完畢。而 <init> 方法則需要顯性的調(diào)用父類的構(gòu)造器。
<clinit>() 方法由編譯器自動生成,但不是必須生成的,只有這個類存在static修飾的變量,或者類中存在靜態(tài)代碼塊但時候,才會自動生成<clinit>()方法。
加載過程總結(jié)
當(dāng)一個符合Java虛擬機規(guī)范的字節(jié)流文件,經(jīng)歷 加載、驗證、準(zhǔn)備、解析、初始化這些階段相互協(xié)作執(zhí)行完成之后,加載階段讀取到的Class字節(jié)流信息,會按虛擬機規(guī)定的格式,在方法區(qū)保存一份,然后Java 堆中,會創(chuàng)建一個 java.lang.Class 類的對象,這個對象描述了這個類所有信息,也提供了這個類在方法區(qū)的訪問入口。?
方法區(qū)中,使用同一加載器的情況下,每個類只會有一份Class字節(jié)流信息;
Java堆中,使用同一加載器的情況下,每個類只會有一份 java.lang.Class 類的對象。
4 類加載器還記得在前面加載階段,通過類的全限定名,獲取該類字節(jié)流數(shù)據(jù)的這個動作么,類加載器就是用來實現(xiàn)這個動作的。當(dāng)年為了滿足瀏覽器上?Java Applet 的需求,Java的開發(fā)團(tuán)隊設(shè)計了類加載器,它獨立于Java虛擬機外部,允許程序員按自身需要自行實現(xiàn)類加載器。這是一項非常優(yōu)秀的創(chuàng)新,它讓同一個類可以實現(xiàn)訪問隔離、OSGi、程序熱部署等等。發(fā)展至今,類加載器已經(jīng)是Java技術(shù)體系的一塊重要基石。
三層類加載器介紹
啟動類加載器(Bootstrap Class Loader):負(fù)責(zé)加載<JAVA_HOME>\lib 目錄,或者被 -Xbootclasspath 參數(shù)制定的路徑,例如 jre/lib/rt.jar 里所有的class文件。由C++實現(xiàn),不是ClassLoader子類。
拓展類加載器(Extension Class Loader):負(fù)責(zé)加載Java平臺中擴展功能的一些jar包,包括<JAVA_HOME>\lib\ext 目錄中 或 java.ext.dirs 指定目錄下的jar包。由Java代碼實現(xiàn)。
應(yīng)用程序類加載器(Application Class Loader):我們自己開發(fā)的應(yīng)用程序,就是由它進(jìn)行加載的,負(fù)責(zé)加載ClassPath路徑下所有jar包。
雙親委派模型
高端的食材往往只需要最簡單的烹飪方式,而保證Java程序穩(wěn)定運行的雙親委派模式,其實也非常簡單:
雙親委派模式其實一句話就可以說清楚:任何一個類加載器在接到一個類的加載請求時,都會先讓其父類進(jìn)行加載,只有父類無法加載(或者沒有父類)的情況下,才嘗試自己加載。

ClassLoader 類中有示例,如下:
雙親委派模型好處
?
在解答這個問題前,需要先了解一個知識點:不同的類加載器,加載同一個類,結(jié)果是虛擬機里會存在兩份這個類的信息,所以當(dāng)判斷這兩個類是否“相等”時,必定是不相等的。
使用雙親委派模式,可以保證,每一個類只會有一個類加載器。例如Java最基礎(chǔ)的Object類,它存放在 rt.jar 之中,這是 Bootstrap 的職責(zé)范圍,當(dāng)向上委派到 Bootstrap 時就會被加載。但如果沒有使用雙親委派模式,可以任由自定義加載器進(jìn)行加載的話,Java這些核心類的API就會被隨意篡改。

好了,Java類加載機制掌握這些就足夠了,再也不用怕面試官問到這類問題了,可以和他掰扯一下了,你們學(xué)會了嗎?