JVM — 类加载机制
1. 引言
java 類被虛擬機(jī)編譯之后成為一個(gè) Class 的字節(jié)碼文件,該字節(jié)碼文件中包含各種描述信息,最終都需要加載到虛擬機(jī)中之后才能運(yùn)行和使用。那么虛擬機(jī)是如何加載這些 Class 文件?Class 文件中的信息進(jìn)入虛擬機(jī)之后會(huì)發(fā)生什么變化?接下來(lái)我們一個(gè)一個(gè)探討。
2. 類加載的時(shí)機(jī)
類的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析 3 個(gè)部分統(tǒng)稱為連接。
在上圖中,加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這 5 個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這個(gè)過(guò)程按部就班的開(kāi)始,中間可以再插入另一個(gè)類的加載過(guò)程。那么,什么情況下需要開(kāi)始類加載過(guò)程的第一個(gè)階段呢?虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有?5 種情況必須立即對(duì)類進(jìn)行「初始化」。
- 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時(shí),如果類沒(méi)有初始化,則需要先觸發(fā)其初始化。生成這 4 條指令的最常見(jiàn)的 java 代碼場(chǎng)景是:使用 new 關(guān)鍵字實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被 final、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法時(shí)。
- 使用 java.lang.reflec 包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)父類沒(méi)有初始化過(guò),則需要先觸發(fā)其父類的初始化
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)執(zhí)行的類(包含 main 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類
- 當(dāng)使用 JDK1.7 的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例后的解析結(jié)果 REF_getStatic、REF_pubStatic、REF_invokeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有進(jìn)行過(guò)初始化,則需要先初始化這個(gè)類。(這一點(diǎn)還不理解是什么)
3. 類加載過(guò)程
了解了類是什么時(shí)候開(kāi)始加載之后,我們來(lái)了解一下類加載的全過(guò)程。也就是加載、驗(yàn)證、準(zhǔn)備、解析和初始化這個(gè) 5 個(gè)階段的具體動(dòng)作。
3.1 加載
注?:「加載」是「類加載」過(guò)程的一個(gè)階段,在加載階段,虛擬機(jī)需要完成下面 3 件事情:
3.2 驗(yàn)證
在加載階段中,Class 文件并不一定要求用 java 源碼編譯而來(lái),可以使用任何途徑產(chǎn)生,甚至包括用十六進(jìn)制編輯器直接編寫(xiě)來(lái)產(chǎn)生 Class 文件。因此,為了保證虛擬機(jī)的安全,驗(yàn)證階段是非常有必要的,驗(yàn)證階段的工作量在虛擬機(jī)的類加載子系統(tǒng)中占了相當(dāng)大的一部分。其大致會(huì)完成下面 4 個(gè)階段的檢驗(yàn)動(dòng)作。
- 文件格式驗(yàn)證
- 元數(shù)據(jù)驗(yàn)證,即文件描述信息是否符合 java 的語(yǔ)法規(guī)則,主要驗(yàn)證類的數(shù)據(jù)類型是否正確,例如這個(gè)類是否有父類,該類的父類是否繼承了不允許被繼承的類等等。
- 字節(jié)碼驗(yàn)證,這個(gè)驗(yàn)證過(guò)程主要是針對(duì)類的方法體,保證被檢驗(yàn)的類方法不會(huì)做出危害虛擬機(jī)的事。
- 符號(hào)引用驗(yàn)證,判斷該類中引用的類信息能否訪問(wèn),或者有權(quán)訪問(wèn)(更具 private、protected 修飾符訪問(wèn))。
3.3 準(zhǔn)備
準(zhǔn)備階段是正式為?類變量?分配內(nèi)存并設(shè)置類變量初始化的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。
這里需要注意兩點(diǎn),首先,這個(gè)階段初始化的變量是類變量,即 static 修飾的變量,不包括實(shí)例變量。實(shí)例變量將會(huì)在對(duì)象初始化時(shí)隨對(duì)象一起分配到 java 堆中。其次,這里的初始化「通常情況」下是數(shù)據(jù)類型的?零值,例如一個(gè)變量?public static int value = 123,其先初始化為 0,等到這個(gè)類首次被初始化之后才變?yōu)?123。
上面說(shuō)了通常情況下是那樣,當(dāng)然也存在一些「不通常的情況」,例如public static final int value = 123。final 修飾的變量在此階段就會(huì)生產(chǎn)對(duì)應(yīng)的值。
3.4 解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。其完成的任務(wù)是驗(yàn)證階段的符號(hào)引用驗(yàn)證。主要由下面 4 種解析過(guò)程
- 類或接口解析
- 字段解析
- 類方法解析
- 接口方法解析
3.5 初始化
在準(zhǔn)備階段,主要是對(duì)類變量進(jìn)行賦值(一般類型賦為 0,boolean 賦為 false 等等),而初始化階段是初始化類變量和其他資源,這是執(zhí)行?類構(gòu)造器<clinit>()?方法的過(guò)程。下面介紹一些可能會(huì)影響到程序運(yùn)行行為的特點(diǎn)和細(xì)節(jié):
- <clinit>() 方法收集類變量的賦值動(dòng)作和執(zhí)行靜態(tài)語(yǔ)句塊的語(yǔ)句。靜態(tài)語(yǔ)句塊只能為定義在語(yǔ)句塊后面的變量賦值,但是不能訪問(wèn)定義在語(yǔ)句塊后面的變量。例如
- <clinit>() 方法與類的構(gòu)造函數(shù)不同,它不需要顯示地調(diào)用父類構(gòu)造函數(shù),虛擬機(jī)保證子類的 <clinit>() 方法執(zhí)行之前,父類的 <clinit>() 已經(jīng)執(zhí)行完畢。因此,第一個(gè)在虛擬機(jī)中被執(zhí)行 <clinit>() 方法的類一定是?java.lang.Object。
- 由于父類的 <clinit>() 方法先執(zhí)行,因此父類定義的靜態(tài)語(yǔ)句塊要先于子類的靜態(tài)語(yǔ)句塊。
- 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>() 方法在多線程的環(huán)境中被正確的加鎖、同步,如果多個(gè)線程同時(shí)初始化一個(gè)類,剛好這個(gè)類的<clinit>() 方法耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程的阻塞。例如
運(yùn)行結(jié)果如下,即一個(gè)線程在死循環(huán)中長(zhǎng)時(shí)間操作,另一個(gè)線程發(fā)生阻塞,一直等待。
Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-0,5,main]init DeadLoopClass4. 類加載器
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的「通過(guò)一個(gè)類的全限定名來(lái)獲取描述此類的二進(jìn)制字節(jié)流」這個(gè)動(dòng)作放在了 java 虛擬機(jī)外部去實(shí)現(xiàn),以便讓?xiě)?yīng)用程序自己決定如何去獲取需要的類。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為「類加載器」。
4.1 類與類加載器
類加載器在 java 程序中起到的作用遠(yuǎn)遠(yuǎn)不限于類的加載階段。在運(yùn)行階段,比較兩個(gè)類是否「相等」,只有在這兩個(gè)類來(lái)源于用一個(gè) Class 文件,被同一個(gè)虛擬機(jī)加載,并且使同一個(gè)類加載器加載,這兩個(gè)類才會(huì)相等。
這里所指的「相等」,包括代表類的 Class 對(duì)象的?equals?方法、isAssignableFrom方法、isInstance?方法的返回結(jié)果,也包括使用?instanceof關(guān)鍵字做對(duì)象所屬關(guān)系判定等情況。
4.2 雙親委派模型
絕大部分 java 程序都會(huì)使用到以下 3 種系統(tǒng)提供的類加載器
- 啟動(dòng)類加載器:這個(gè)類加載器負(fù)責(zé)將存放在?<JAVA_HOME>\lib?目錄中的類庫(kù)加載到虛擬機(jī)內(nèi)存中。
- 擴(kuò)展類加載器:這個(gè)加載器由?sun.misc.Launcher$ExtClassLoader?實(shí)現(xiàn),它負(fù)責(zé)加載?<JAVA_HOME>\lib\ext?目錄中,或被 java.ext.dirs 系統(tǒng)變量所指定的所有類庫(kù)。
- 應(yīng)用程序類加載器:這個(gè)類庫(kù)加載器由?sun.misc.Launcher$AppClassLoader?實(shí)現(xiàn),它負(fù)責(zé)加載用戶路徑上(ClassPath)所指定的類庫(kù)。如果應(yīng)用程序中沒(méi)有自定的類加載器,一般情況下這個(gè)就是默認(rèn)的類加載器。
我們的應(yīng)用程序都是由這 3 種類加載器相互配合進(jìn)行加載的,如果有必要,還可以自定義類加載器。這些類加載器之間的關(guān)系如下圖所示,這種層次關(guān)系被稱為類加載器的雙親委派模型。
雙親委派模型的工作過(guò)程是:如果一個(gè)類加載器收到了類加載的要求,它首先自己不會(huì)去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此,所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法加載這個(gè)請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
這種模型的一個(gè)好處就是由于類加載器有一種層次關(guān)系,導(dǎo)致類也有一種層次關(guān)系,從而有了優(yōu)先級(jí)。比如類java.lang.Object,它存放在rt.jar中,無(wú)論哪個(gè)類加載器要加載這個(gè)類,最終都要委派給啟動(dòng)類加載器去加載,因此Object類在各個(gè)類加載器環(huán)境中都是同一個(gè)類。相反,如果沒(méi)有使用雙親委派模型,由各個(gè)類加載器去自行加載,如果用戶自己編寫(xiě)了一個(gè)稱為java.lang.Object的類,并放在程序的ClassPath中,那系統(tǒng)將會(huì)有多個(gè)不同的Object類,java類型體系中最基礎(chǔ)的行為也就沒(méi)有辦法保證了。
?
自定義類加載器
為什么需要自定義類加載器?
網(wǎng)上的大部分自定義類加載器文章,幾乎都是貼一段實(shí)現(xiàn)代碼,然后分析一兩句自定義ClassLoader的原理。但是我覺(jué)得首先得把為什么需要自定義加載器這個(gè)問(wèn)題搞清楚,因?yàn)槿绻幻靼姿淖饔玫那闆r下,還要去學(xué)習(xí)它顯然是很讓人困惑的。
首先介紹自定義類的應(yīng)用場(chǎng)景:
(1)加密:Java代碼可以輕易的被反編譯,如果你需要把自己的代碼進(jìn)行加密以防止反編譯,可以先將編譯后的代碼用某種加密算法加密,類加密后就不能再用Java的ClassLoader去加載類了,這時(shí)就需要自定義ClassLoader在加載類的時(shí)候先解密類,然后再加載。?
(2)從非標(biāo)準(zhǔn)的來(lái)源加載代碼:如果你的字節(jié)碼是放在數(shù)據(jù)庫(kù)、甚至是在云端,就可以自定義類加載器,從指定的來(lái)源加載類。?
(3)以上兩種情況在實(shí)際中的綜合運(yùn)用:比如你的應(yīng)用需要通過(guò)網(wǎng)絡(luò)來(lái)傳輸 Java 類的字節(jié)碼,為了安全性,這些字節(jié)碼經(jīng)過(guò)了加密處理。這個(gè)時(shí)候你就需要自定義類加載器來(lái)從某個(gè)網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進(jìn)行解密和驗(yàn)證,最后定義出在Java虛擬機(jī)中運(yùn)行的類。
1. 雙親委派模型
在實(shí)現(xiàn)自己的ClassLoader之前,我們先了解一下系統(tǒng)是如何加載類的,那么就不得不介紹雙親委派模型的實(shí)現(xiàn)過(guò)程。
//雙親委派模型的工作過(guò)程源碼 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{// First, check if the class has already been loadedClass c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader//父類加載器無(wú)法完成類加載請(qǐng)求 }if (c == null) {// If still not found, then invoke findClass in order to find the class//子加載器進(jìn)行類加載 c = findClass(name);}}if (resolve) {//判斷是否需要鏈接過(guò)程,參數(shù)傳入 resolveClass(c);}return c; }2.?雙親委派模型的工作過(guò)程如下:
(1)當(dāng)前類加載器從自己已經(jīng)加載的類中查詢是否此類已經(jīng)加載,如果已經(jīng)加載則直接返回原來(lái)已經(jīng)加載的類。
(2)如果沒(méi)有找到,就去委托父類加載器去加載(如代碼c = parent.loadClass(name,?false)所示)。父類加載器也會(huì)采用同樣的策略,查看自己已經(jīng)加載過(guò)的類中是否包含這個(gè)類,有就返回,沒(méi)有就委托父類的父類去加載,一直到啟動(dòng)類加載器。因?yàn)槿绻讣虞d器為空了,就代表使用啟動(dòng)類加載器作為父加載器去加載。
(3)如果啟動(dòng)類加載器加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),則會(huì)拋出一個(gè)異常ClassNotFoundException,然后再調(diào)用當(dāng)前加載器的findClass()方法進(jìn)行加載。?
?
3.?雙親委派模型的好處:
(1)主要是為了安全性,避免用戶自己編寫(xiě)的類動(dòng)態(tài)替換?Java的一些核心類,比如?String。
(2)同時(shí)也避免了類的重復(fù)加載,因?yàn)?JVM中區(qū)分不同類,不僅僅是根據(jù)類名,相同的?class文件被不同的?ClassLoader加載就是不同的兩個(gè)類。?
2. 自定義類加載器
(1)從上面源碼看出,調(diào)用loadClass時(shí)會(huì)先根據(jù)委派模型在父加載器中加載,如果加載失敗,則會(huì)調(diào)用當(dāng)前加載器的findClass來(lái)完成加載。
(2)因此我們自定義的類加載器只需要繼承ClassLoader,并覆蓋findClass方法,下面是一個(gè)實(shí)際例子,在該例中我們用自定義的類加載器去加載我們事先準(zhǔn)備好的class文件。
?
2.1?自定義一個(gè)People.java類做例子
public class People { //該類寫(xiě)在記事本里,在用javac命令行編譯成class文件,放在d盤(pán)根目錄下private String name;public People() {}public People(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String toString() {return "I am a people, my name is " + name;}}2.2?自定義類加載器
自定義一個(gè)類加載器,需要繼承ClassLoader類,并實(shí)現(xiàn)findClass方法。其中defineClass方法可以把二進(jìn)制流字節(jié)組成的文件轉(zhuǎn)換為一個(gè)java.lang.Class(只要二進(jìn)制字節(jié)流的內(nèi)容符合Class文件規(guī)范)。
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel;public class MyClassLoader extends ClassLoader{public MyClassLoader() {super(null);}public MyClassLoader(ClassLoader parent) {super(parent);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {File file = new File("D:/People.class");try {byte[] bytes = getClassBytes(file);//defineClass方法可以把二進(jìn)制流字節(jié)組成的文件轉(zhuǎn)換為一個(gè)java.lang.ClassClass<?> c = this.defineClass(name, bytes, 0, bytes.length);return c;} catch (ClassFormatError e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}return super.findClass(name);}private byte[] getClassBytes(File file) throws Exception{// 這里要讀入.class的字節(jié),因此要使用字節(jié)流FileInputStream fis = new FileInputStream(file);FileChannel fc = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();WritableByteChannel wbc = Channels.newChannel(baos);ByteBuffer by = ByteBuffer.allocate(1024);while (true){int i = fc.read(by);if (i == 0 || i == -1)break;by.flip();wbc.write(by);by.clear();}fis.close();return baos.toByteArray();}}2.3?在主函數(shù)里使用
MyClassLoader mcl = new MyClassLoader(); Class<?> clazz = Class.forName("com.gdut.classLoader1.People", true, mcl); Object obj = clazz.newInstance();System.out.println(obj); System.out.println(obj.getClass().getClassLoader());//打印出我們的自定義類加載器2.4?運(yùn)行結(jié)果
?
資料:??https://www.liangzl.com/get-article-detail-13809.html
? https://blog.csdn.net/SEU_Calvin/article/details/52315125
?
轉(zhuǎn)載于:https://www.cnblogs.com/myseries/p/10913456.html
總結(jié)
以上是生活随笔為你收集整理的JVM — 类加载机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在js中获取input中的value
- 下一篇: mongoDB初识一二三