您好,登錄后才能下訂單哦!
這篇文章主要介紹“JVM類(lèi)加載子系統(tǒng)的方法”,在日常操作中,相信很多人在JVM類(lèi)加載子系統(tǒng)的方法問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”JVM類(lèi)加載子系統(tǒng)的方法”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
完整圖如下
如果自己想手寫(xiě)一個(gè)Java虛擬機(jī)的話(huà),主要考慮哪些結(jié)構(gòu)呢?
類(lèi)加載器
執(zhí)行引擎
類(lèi)加載器子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載Class文件,class文件在文件開(kāi)頭有特定的文件標(biāo)識(shí)。
ClassLoader只負(fù)責(zé)class文件的加載,至于它是否可以運(yùn)行,則由Execution Engine(執(zhí)行引擎)決定。
加載的類(lèi)信息存放于一塊稱(chēng)為方法區(qū)的內(nèi)存空間。除了類(lèi)的信息外,方法區(qū)中還會(huì)存放運(yùn)行時(shí)常量池信息,可能還包括字符串字面量和數(shù)字常量(這部分常量信息是Class文件中常量池部分的內(nèi)存映射)
class file存在于本地硬盤(pán)上,可以理解為設(shè)計(jì)師畫(huà)在紙上的模板,而最終這個(gè)模板在執(zhí)行的時(shí)候是要加載到JVM當(dāng)中來(lái)根據(jù)這個(gè)文件實(shí)例化出n個(gè)一模一樣的實(shí)例。
class file加載到JVM中,被稱(chēng)為DNA元數(shù)據(jù)模板,放在方法區(qū)。
在.class文件->JVM->最終成為元數(shù)據(jù)模板,此過(guò)程就要一個(gè)運(yùn)輸工具(類(lèi)裝載器Class Loader),扮演一個(gè)快遞員的角色。
例如下面的一段簡(jiǎn)單的代碼
public class HelloLoader { public static void main(String[] args) { System.out.println("我已經(jīng)被加載啦"); } }
它的加載過(guò)程是怎么樣的呢?
完整的流程圖如下所示
通過(guò)一個(gè)類(lèi)的全限定名獲取定義此類(lèi)的二進(jìn)制字節(jié)流
將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪(fǎng)問(wèn)入口
從本地系統(tǒng)中直接加載
通過(guò)網(wǎng)絡(luò)獲取,典型場(chǎng)景:Web Applet
從zip壓縮包中讀取,成為日后jar、war格式的基礎(chǔ)
運(yùn)行時(shí)計(jì)算生成,使用最多的是:動(dòng)態(tài)代理技術(shù)
由其他文件生成,典型場(chǎng)景:JSP應(yīng)用從專(zhuān)有數(shù)據(jù)庫(kù)中提取.class文件,比較少見(jiàn)
從加密文件中獲取,典型的防Class文件被反編譯的保護(hù)措施
目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,保證被加載類(lèi)的正確性,不會(huì)危害虛擬機(jī)自身安全。
主要包括四種驗(yàn)證,文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證。
工具:Binary Viewer查看
如果出現(xiàn)不合法的字節(jié)碼文件,那么將會(huì)驗(yàn)證不通過(guò)
同時(shí)我們可以通過(guò)安裝IDEA的插件,來(lái)查看我們的Class文件
安裝完成后,我們編譯完一個(gè)class文件后,點(diǎn)擊view即可顯示我們安裝的插件來(lái)查看字節(jié)碼方法了
為類(lèi)變量分配內(nèi)存并且設(shè)置該類(lèi)變量的默認(rèn)初始值,即零值。
這里不包含用final修飾的static,因?yàn)閒inal在編譯的時(shí)候就會(huì)分配了,準(zhǔn)備階段會(huì)顯式初始化;
這里不會(huì)為實(shí)例變量分配初始化,類(lèi)變量會(huì)分配在方法區(qū)中,而實(shí)例變量是會(huì)隨著對(duì)象一起分配到Java堆中。
例如下面這段代碼
public class HelloApp { private static int a = 1; // 準(zhǔn)備階段為0,在下個(gè)階段,也就是初始化的時(shí)候才是1 public static void main(String[] args) { System.out.println(a); } }
上面的變量a在準(zhǔn)備階段會(huì)賦初始值,但不是1,而是0。
將常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用的過(guò)程。
事實(shí)上,解析操作往往會(huì)伴隨著JVM在執(zhí)行完初始化之后再執(zhí)行。
符號(hào)引用就是一組符號(hào)來(lái)描述所引用的目標(biāo)。符號(hào)引用的字面量形式明確定義在《java虛擬機(jī)規(guī)范》的class文件格式中。直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。
解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型等。對(duì)應(yīng)常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
初始化階段就是執(zhí)行類(lèi)構(gòu)造器法<clinit>()的過(guò)程。
此方法不需定義,是javac編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)代碼塊中的語(yǔ)句合并而來(lái)。也就是說(shuō),當(dāng)我們代碼中包含static變量的時(shí)候,就會(huì)有clinit方法
構(gòu)造器方法中指令按語(yǔ)句在源文件中出現(xiàn)的順序執(zhí)行。
<clinit>()不同于類(lèi)的構(gòu)造器。(關(guān)聯(lián):構(gòu)造器是虛擬機(jī)視角下的<init>())
若該類(lèi)具有父類(lèi),JVM會(huì)保證子類(lèi)的<clinit>()執(zhí)行前,父類(lèi)的<clinit>()已經(jīng)執(zhí)行完畢。
虛擬機(jī)必須保證一個(gè)類(lèi)的<clinit>()方法在多線(xiàn)程下被同步加鎖
任何一個(gè)類(lèi)在聲明后,都有生成一個(gè)構(gòu)造器,默認(rèn)是空參構(gòu)造器
public class ClassInitTest { private static int num = 1; static { num = 2; number = 20; System.out.println(num); System.out.println(number); //報(bào)錯(cuò),非法的前向引用 } private static int number = 10; public static void main(String[] args) { System.out.println(ClassInitTest.num); // 2 System.out.println(ClassInitTest.number); // 10 } }
關(guān)于涉及到父類(lèi)時(shí)候的變量賦值過(guò)程
public class ClinitTest1 { static class Father { public static int A = 1; static { A = 2; } } static class Son extends Father { public static int b = A; } public static void main(String[] args) { System.out.println(Son.b); } }
我們輸出結(jié)果為 2,也就是說(shuō)首先加載ClinitTest1的時(shí)候,會(huì)找到main方法,然后執(zhí)行Son的初始化,但是Son繼承了Father,因此還需要執(zhí)行Father的初始化,同時(shí)將A賦值為2。我們通過(guò)反編譯得到Father的加載過(guò)程,首先我們看到原來(lái)的值被賦值成1,然后又被復(fù)制成2,最后返回
iconst_1 putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A> iconst_2 putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A> return
虛擬機(jī)必須保證一個(gè)類(lèi)的<clinit>()方法在多線(xiàn)程下被同步加鎖。
public class DeadThreadTest { public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 線(xiàn)程t1開(kāi)始"); new DeadThread(); }, "t1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 線(xiàn)程t2開(kāi)始"); new DeadThread(); }, "t2").start(); } } class DeadThread { static { if (true) { System.out.println(Thread.currentThread().getName() + "\t 初始化當(dāng)前類(lèi)"); while(true) { } } } }
上面的代碼,輸出結(jié)果為
線(xiàn)程t1開(kāi)始 線(xiàn)程t2開(kāi)始 線(xiàn)程t2 初始化當(dāng)前類(lèi)
從上面可以看出初始化后,只能夠執(zhí)行一次初始化,這也就是同步加鎖的過(guò)程
JVM支持兩種類(lèi)型的類(lèi)加載器 。分別為引導(dǎo)類(lèi)加載器(Bootstrap ClassLoader)和自定義類(lèi)加載器(User-Defined ClassLoader)。
從概念上來(lái)講,自定義類(lèi)加載器一般指的是程序中由開(kāi)發(fā)人員自定義的一類(lèi)類(lèi)加載器,但是Java虛擬機(jī)規(guī)范卻沒(méi)有這么定義,而是將所有派生于抽象類(lèi)ClassLoader的類(lèi)加載器都劃分為自定義類(lèi)加載器。
無(wú)論類(lèi)加載器的類(lèi)型如何劃分,在程序中我們最常見(jiàn)的類(lèi)加載器始終只有3個(gè),如下所示:
這里的四者之間是包含關(guān)系,不是上層和下層,也不是子系統(tǒng)的繼承關(guān)系。
我們通過(guò)一個(gè)類(lèi),獲取它不同的加載器
public class ClassLoaderTest { public static void main(String[] args) { // 獲取系統(tǒng)類(lèi)加載器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // 獲取其上層的:擴(kuò)展類(lèi)加載器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // 試圖獲取 根加載器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // 獲取自定義加載器 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); // 獲取String類(lèi)型的加載器 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); } }
得到的結(jié)果,從結(jié)果可以看出 根加載器無(wú)法直接通過(guò)代碼獲取,同時(shí)目前用戶(hù)代碼所使用的加載器為系統(tǒng)類(lèi)加載器。同時(shí)我們通過(guò)獲取String類(lèi)型的加載器,發(fā)現(xiàn)是null,那么說(shuō)明String類(lèi)型是通過(guò)根加載器進(jìn)行加載的,
也就是說(shuō)Java的核心類(lèi)庫(kù)都是使用根加載器進(jìn)行加載的。
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1540e19d null sun.misc.Launcher$AppClassLoader@18b4aac2 null
這個(gè)類(lèi)加載使用C/C++語(yǔ)言實(shí)現(xiàn)的,嵌套在JVM內(nèi)部。
它用來(lái)加載Java的核心庫(kù)(JAVAHOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內(nèi)容),用于提供JVM自身需要的類(lèi)
并不繼承自java.lang.ClassLoader,沒(méi)有父加載器。
加載擴(kuò)展類(lèi)和應(yīng)用程序類(lèi)加載器,并指定為他們的父類(lèi)加載器。
出于安全考慮,Bootstrap啟動(dòng)類(lèi)加載器只加載包名為java、javax、sun等開(kāi)頭的類(lèi)
使用代碼獲取不到
Java語(yǔ)言編寫(xiě),由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)。
派生于ClassLoader類(lèi)
父類(lèi)加載器為啟動(dòng)類(lèi)加載器
從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類(lèi)庫(kù),或從JDK的安裝目錄的jre/lib/ext子目錄(擴(kuò)展目錄)下加載類(lèi)庫(kù)。如果用戶(hù)創(chuàng)建的JAR放在此目錄下,也會(huì)自動(dòng)由擴(kuò)展類(lèi)加載器加載。
java語(yǔ)言編寫(xiě),由sun.misc.Launchers$AppClassLoader實(shí)現(xiàn)
派生于ClassLoader類(lèi)
父類(lèi)加載器為擴(kuò)展類(lèi)加載器
它負(fù)責(zé)加載環(huán)境變量classpath或系統(tǒng)屬性java.class.path指定路徑下的類(lèi)庫(kù)
該類(lèi)加載是程序中默認(rèn)的類(lèi)加載器,一般來(lái)說(shuō),Java應(yīng)用的類(lèi)都是由它來(lái)完成加載
通過(guò)classLoader#getSystemClassLoader()方法可以獲取到該類(lèi)加載器
在Java的日常應(yīng)用程序開(kāi)發(fā)中,類(lèi)的加載幾乎是由上述3種類(lèi)加載器相互配合執(zhí)行的,在必要時(shí),我們還可以自定義類(lèi)加載器,來(lái)定制類(lèi)的加載方式。
為什么要自定義類(lèi)加載器?哪些場(chǎng)景?
隔離加載類(lèi)
修改類(lèi)加載的方式
擴(kuò)展加載源
防止源碼泄漏
用戶(hù)自定義類(lèi)加載器實(shí)現(xiàn)步驟:
開(kāi)發(fā)人員可以通過(guò)繼承抽象類(lèi)java.lang.ClassLoader類(lèi)的方式,實(shí)現(xiàn)自己的類(lèi)加載器,以滿(mǎn)足一些特殊的需求
在JDK1.2之前,在自定義類(lèi)加載器時(shí),總會(huì)去繼承ClassLoader類(lèi)并重寫(xiě)loadClass()方法,從而實(shí)現(xiàn)自定義的類(lèi)加載類(lèi),但是在JDK1.2之后已不再建議用戶(hù)去覆蓋loadclass()方法,而是建議把自定義的類(lèi)加載邏輯寫(xiě)在findclass()方法中
在編寫(xiě)自定義類(lèi)加載器時(shí),如果沒(méi)有太過(guò)于復(fù)雜的需求,可以直接繼承URLClassLoader類(lèi),這樣就可以避免自己去編寫(xiě)findclass()方法及其獲取字節(jié)碼流的方式,使自定義類(lèi)加載器編寫(xiě)更加簡(jiǎn)潔。加載后是byte[],使用defineClass還原。
剛剛我們通過(guò)概念了解到了,根加載器只能夠加載 java /lib目錄下的class,我們通過(guò)下面代碼驗(yàn)證一下
public class ClassLoaderTest1 { public static void main(String[] args) { System.out.println("*********啟動(dòng)類(lèi)加載器************"); // 獲取BootstrapClassLoader 能夠加載的API的路徑 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); } // 從上面路徑中,隨意選擇一個(gè)類(lèi),來(lái)看看他的類(lèi)加載器是什么:得到的是null,說(shuō)明是 根加載器 ClassLoader classLoader = Provider.class.getClassLoader(); } }
得到的結(jié)果
*********啟動(dòng)類(lèi)加載器************ file:/E:/Software/JDK1.8/Java/jre/lib/resources.jar file:/E:/Software/JDK1.8/Java/jre/lib/rt.jar file:/E:/Software/JDK1.8/Java/jre/lib/sunrsasign.jar file:/E:/Software/JDK1.8/Java/jre/lib/jsse.jar file:/E:/Software/JDK1.8/Java/jre/lib/jce.jar file:/E:/Software/JDK1.8/Java/jre/lib/charsets.jar file:/E:/Software/JDK1.8/Java/jre/lib/jfr.jar file:/E:/Software/JDK1.8/Java/jre/classes null
ClassLoader類(lèi),它是一個(gè)抽象類(lèi),其后所有的類(lèi)加載器都繼承自ClassLoader(不包括啟動(dòng)類(lèi)加載器)
sun.misc.Launcher 它是一個(gè)java虛擬機(jī)的入口應(yīng)用
獲取當(dāng)前ClassLoader:clazz.getClassLoader()
獲取當(dāng)前線(xiàn)程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
獲取系統(tǒng)的ClassLoader:ClassLoader.getSystemClassLoader()
獲取調(diào)用者的ClassLoader:DriverManager.getCallerClassLoader()
Java虛擬機(jī)對(duì)class文件采用的是按需加載的方式,也就是說(shuō)當(dāng)需要使用該類(lèi)時(shí)才會(huì)將它的class文件加載到內(nèi)存生成class對(duì)象。而且加載某個(gè)類(lèi)的class文件時(shí),Java虛擬機(jī)采用的是雙親委派模式,即把請(qǐng)求交由父類(lèi)處理,它是一種任務(wù)委派模式。
如果一個(gè)類(lèi)加載器收到了類(lèi)加載請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給父類(lèi)的加載器去執(zhí)行;
如果父類(lèi)加載器還存在其父類(lèi)加載器,則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類(lèi)加載器;
如果父類(lèi)加載器可以完成類(lèi)加載任務(wù),就成功返回,倘若父類(lèi)加載器無(wú)法完成此加載任務(wù),子加載器才會(huì)嘗試自己去加載,這就是雙親委派模式。
當(dāng)我們加載jdbc.jar 用于實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接的時(shí)候,首先我們需要知道的是 jdbc.jar是基于SPI接口進(jìn)行實(shí)現(xiàn)的,所以在加載的時(shí)候,會(huì)進(jìn)行雙親委派,最終從根加載器中加載 SPI核心類(lèi),然后在加載SPI接口類(lèi),接著在進(jìn)行反向委派,通過(guò)線(xiàn)程上下文類(lèi)加載器進(jìn)行實(shí)現(xiàn)類(lèi) jdbc.jar的加載。
通過(guò)上面的例子,我們可以知道,雙親機(jī)制可以
避免類(lèi)的重復(fù)加載
保護(hù)程序安全,防止核心API被隨意篡改
自定義類(lèi):java.lang.String
自定義類(lèi):java.lang.ShkStart(報(bào)錯(cuò):阻止創(chuàng)建 java.lang開(kāi)頭的類(lèi))
自定義string類(lèi),但是在加載自定義String類(lèi)的時(shí)候會(huì)率先使用引導(dǎo)類(lèi)加載器加載,而引導(dǎo)類(lèi)加載器在加載的過(guò)程中會(huì)先加載jdk自帶的文件(rt.jar包中java\lang\String.class),報(bào)錯(cuò)信息說(shuō)沒(méi)有main方法,就是因?yàn)榧虞d的是rt.jar包中的string類(lèi)。這樣可以保證對(duì)java核心源代碼的保護(hù),這就是沙箱安全機(jī)制。
在JVM中表示兩個(gè)class對(duì)象是否為同一個(gè)類(lèi)存在兩個(gè)必要條件:
類(lèi)的完整類(lèi)名必須一致,包括包名。
加載這個(gè)類(lèi)的ClassLoader(指ClassLoader實(shí)例對(duì)象)必須相同。
換句話(huà)說(shuō),在JVM中,即使這兩個(gè)類(lèi)對(duì)象(class對(duì)象)來(lái)源同一個(gè)Class文件,被同一個(gè)虛擬機(jī)所加載,但只要加載它們的ClassLoader實(shí)例對(duì)象不同,那么這兩個(gè)類(lèi)對(duì)象也是不相等的。
JVM必須知道一個(gè)類(lèi)型是由啟動(dòng)加載器加載的還是由用戶(hù)類(lèi)加載器加載的。如果一個(gè)類(lèi)型是由用戶(hù)類(lèi)加載器加載的,那么JVM會(huì)將這個(gè)類(lèi)加載器的一個(gè)引用作為類(lèi)型信息的一部分保存在方法區(qū)中。當(dāng)解析一個(gè)類(lèi)型到另一個(gè)類(lèi)型的引用的時(shí)候,JVM需要保證這兩個(gè)類(lèi)型的類(lèi)加載器是相同的。
Java程序?qū)︻?lèi)的使用方式分為:王動(dòng)使用和被動(dòng)使用。
主動(dòng)使用,又分為七種情況:
創(chuàng)建類(lèi)的實(shí)例
訪(fǎng)問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
調(diào)用類(lèi)的靜態(tài)方法I
反射(比如:Class.forName("com.atguigu.Test"))
初始化一個(gè)類(lèi)的子類(lèi)
Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi)
JDK7開(kāi)始提供的動(dòng)態(tài)語(yǔ)言支持:
java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF getStatic、REF putStatic、REF invokeStatic句柄對(duì)應(yīng)的類(lèi)沒(méi)有初始化,則初始化
除了以上七種情況,其他使用Java類(lèi)的方式都被看作是對(duì)類(lèi)的被動(dòng)使用,都不會(huì)導(dǎo)致類(lèi)的初始化。
到此,關(guān)于“JVM類(lèi)加載子系統(tǒng)的方法”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。