本篇講解jvm模塊的類加載機(jī)制,學(xué)習(xí)jvm,就必須要知道類是怎么加載的。
假設(shè)有這樣一個(gè)類:
package com.manong.jvm;public class Math { public static final int initData = 666; public static User user = new User(); public int compute() { //一個(gè)方法對(duì)應(yīng)一塊棧幀內(nèi)存區(qū)域 int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); }
以上面的類為例,直接來看這個(gè)類是怎么進(jìn)入jvm,并運(yùn)行的。
運(yùn)行流程如下:
(1) javac編譯階段,javac會(huì)把我們寫的java文件編譯成class類型文件.
(2) Windows系統(tǒng)下java.exe調(diào)用底層的jvm.dll文件創(chuàng)建虛擬機(jī).
(3) 虛擬機(jī)首先創(chuàng)建一個(gè)引導(dǎo)類加載器.
(4) 由引導(dǎo)類加載器加載sun.misc.Launcher類(啟動(dòng)器類),先看下這個(gè)類的源碼:
//此源碼只有相關(guān)的部分,并且省略了異常捕獲相關(guān)的代碼public class Launcher { private static Launcher launcher = new Launcher(); private ClassLoader loader; public static Launcher getLauncher() { return launcher; } public Launcher() { Launcher.ExtClassLoader var1; var1=Launcher.ExtClassLoader.getExtClassLoader(); this.loader=Launcher.AppClassLoader.getAppClassLoader(var1); Thread.currentThread().setContextClassLoader(this.loader); } public ClassLoader getClassLoader() { return this.loader; }}
(5) 調(diào)用Launcher實(shí)例的getClassLoader方法,獲取應(yīng)用類加載器開 始加載類,請(qǐng)注意,不管是加載什么類,這里都是用獲取到應(yīng)用類加載器去加載,利用內(nèi)部的委派機(jī)制向上委派;
(6) 調(diào)用應(yīng)用類加載器的loadClass方法,進(jìn)入選擇類加載器階段;
(7) 調(diào)用上層的findClass方法,進(jìn)入類加載環(huán)節(jié);
(8) 加載完畢,開始執(zhí)行代碼;
注意:應(yīng)用類加載器AppClassLoader和擴(kuò)展類加載器ExtClassLoader其實(shí)都是Launcher類的一個(gè)內(nèi)部類,可以自己去源碼中看下
上面就是類加載到j(luò)vm并運(yùn)行的過程,接下來我們重點(diǎn)了解下上面的第6、7 點(diǎn)。
public class TestJDKClassLoader { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName()); System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName()); System.out.println(); ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); ClassLoader extClassloader = appClassLoader.getParent(); ClassLoader bootstrapLoader = extClassloader.getParent(); System.out.println("the bootstrapLoader : " + bootstrapLoader); System.out.println("the extClassloader : " + extClassloader); System.out.println("the appClassLoader : " + appClassLoader); System.out.println(); System.out.println("bootstrapLoader加載以下文件:"); URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i]); } System.out.println(); System.out.println("extClassloader加載以下文件:"); System.out.println(System.getProperty("java.ext.dirs")); System.out.println(); System.out.println("appClassLoader加載以下文件:"); System.out.println(System.getProperty("java.class.path")); }}運(yùn)行結(jié)果:nullsun.misc.Launcher$ExtClassLoadersun.misc.Launcher$AppClassLoaderthe bootstrapLoader : nullthe extClassloader : sun.misc.Launcher$ExtClassLoader@3764951dthe appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dcbootstrapLoader加載以下文件:file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jarfile:/D:/dev/Java/jdk1.8.0_45/jre/classesextClassloader加載以下文件:D:/dev/Java/jdk1.8.0_45/jre/lib/ext;C:/Windows/Sun/Java/lib/extappClassLoader加載以下文件:D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/deploy.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/access-bridge-64.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/cldrdata.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/dnsns.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/jaccess.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/jfxrt.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/localedata.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/nashorn.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/sunec.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/sunjce_provider.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/sunmscapi.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/sunpkcs11.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/ext/zipfs.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/javaws.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/jfxswt.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/management-agent.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/plugin.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jar;D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jar;D:/ideaProjects/project-all/target/classes;C:/Users/zhuge/.m2/repository/org/apache/zookeeper/zookeeper/3.4.12/zookeeper-3.4.12.jar;C:/Users/zhuge/.m2/repository/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar;C:/Users/zhuge/.m2/repository/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar;C:/Users/zhuge/.m2/repository/log4j/log4j/1.2.17/log4j-1.2.17.jar;C:/Users/zhuge/.m2/repository/jline/jline/0.9.94/jline-0.9.94.jar;C:/Users/zhuge/.m2/repository/org/apache/yetus/audience-annotations/0.5.0/audience-annotations-0.5.0.jar;C:/Users/zhuge/.m2/repository/io/netty/netty/3.10.6.Final/netty-3.10.6.Final.jar;C:/Users/zhuge/.m2/repository/com/google/guava/guava/22.0/guava-22.0.jar;C:/Users/zhuge/.m2/repository/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar;C:/Users/zhuge/.m2/repository/com/google/errorprone/error_prone_annotations/2.0.18/error_prone_annotations-2.0.18.jar;C:/Users/zhuge/.m2/repository/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar;C:/Users/zhuge/.m2/repository/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar;D:/dev/IntelliJ IDEA 2018.3.2/lib/idea_rt.jar
上面的代碼足以讓你看清,每種類加載器加載的是哪個(gè)路徑下的類。
上面的步驟中提過JVM默認(rèn)使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實(shí)例加載我們的應(yīng)用程序,那么我們知道我們自己寫的類是由AppClassLoader加載,這個(gè)沒有問題,那一些類庫(kù)中類怎么加載呢,這就要依托于雙親委派機(jī)制了。
加載某個(gè)類時(shí)會(huì)先委托父加載器尋找目標(biāo)類,找不到再委托上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標(biāo)類,則在自己的類加載路徑中查找并載入目標(biāo)類。比如我們的Math類,最先會(huì)找應(yīng)用程序類加載器加載,應(yīng)用程序類加載器會(huì)先委托擴(kuò)展類加載器加載,擴(kuò)展類加載器再委托引導(dǎo)類加載器,頂層引導(dǎo)類加載器在自己的類加載路徑里找了半天沒找到Math類,則向下退回加載Math類的請(qǐng)求,擴(kuò)展類加載器收到回復(fù)就自己加載,在自己的類加載路徑里找了半天也沒找到Math類,又向下退回Math類的加載請(qǐng)求給應(yīng)用程序類加載器,應(yīng)用程序類加載器于是在自己的類加載路徑里找Math類,結(jié)果找到了就自己加載了。。雙親委派機(jī)制說簡(jiǎn)單點(diǎn)就是,先找父親加載,不行再由兒子自己加載
我們來看下應(yīng)用程序類加載器AppClassLoader加載類的雙親委派機(jī)制源碼,AppClassLoader的loadClass方法最終會(huì)調(diào)用其父類ClassLoader的loadClass方法,該方法的大體邏輯如下:首先,檢查一下指定名稱的類是否已經(jīng)加載過,如果加載過了,就不需要再加載,直接返回。如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加載器來加載。如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調(diào)用當(dāng)前類加載器的findClass方法來完成類加載。
//ClassLoader的loadClass方法,里面實(shí)現(xiàn)了雙親委派機(jī)制protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // 檢查當(dāng)前類加載器是否已經(jīng)加載了該類 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //如果當(dāng)前加載器父加載器不為空則委托父加載器加載該類 c = parent.loadClass(name, false); } else { //如果當(dāng)前加載器父加載器為空則委托引導(dǎo)類加載器加載該類 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //都會(huì)調(diào)用URLClassLoader的findClass方法在加載器的類路徑里查找并加載該類 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) { //不會(huì)執(zhí)行 resolveClass(c); } return c; }
為什么要設(shè)計(jì)雙親委派機(jī)制?
package java.lang;public class String { public static void main(String[] args) { System.out.println("**************My String Class**************"); }}運(yùn)行結(jié)果:錯(cuò)誤: 在類 java.lang.String 中找不到 main 方法, 請(qǐng)將 main 方法定義為......
全盤負(fù)責(zé)委托機(jī)制 “全盤負(fù)責(zé)”是指當(dāng)一個(gè)ClassLoder裝載一個(gè)類時(shí),除非顯示的使用另外一個(gè)ClassLoder,該類所依賴及引用的類也由這個(gè)ClassLoder載入。
雙親委派模型并不是一個(gè)具有強(qiáng)制性約束的模型,而是Java設(shè)計(jì)者推薦給開發(fā)者們的類加載器實(shí)現(xiàn)方式。在Java的世界中大部分的類加載器都遵循這個(gè)模型,但也有例外的情況,直到Java模塊化出現(xiàn)為止,雙親委派模型主要出現(xiàn)過3次較大規(guī)模“被破壞”的情況。
雙親委派模型的第一次“被破壞”其實(shí)發(fā)生在雙親委派模型出現(xiàn)之前——即JDK 1.2面世以前的“遠(yuǎn)古”時(shí)代。由于雙親委派模型在JDK 1.2之后才被引入,但是類加載器的概念和抽象類java.lang.ClassLoader則在Java的第一個(gè)版本中就已經(jīng)存在,面對(duì)已經(jīng)存在的用戶自定義類加載器的代碼,Java設(shè)計(jì)者們引入雙親委派模型時(shí)不得不做出一些妥協(xié),為了兼容這些已有代碼,無法再以技術(shù)手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個(gè)新的protected方法findClass(),并引導(dǎo)用戶編寫的類加載邏輯時(shí)盡可能去重寫這個(gè)方法,而不是在loadClass()中編寫代碼。
上節(jié)我們已經(jīng)分析過loadClass()方法,雙親委派的具體邏輯就實(shí)現(xiàn)在這里面,按照loadClass()方法的邏輯,如果父類加載失敗,會(huì)自動(dòng)調(diào)用自己的findClass()方法來完成加載,這樣既不影響用戶按照自己的意愿去加載類,又可以保證新寫出來的類加載器是符合雙親委派規(guī)則的。
雙親委派模型的第二次“被破壞”是由這個(gè)模型自身的缺陷導(dǎo)致的,雙親委派很好地解決了各個(gè)類加載器協(xié)作時(shí)基礎(chǔ)類型的一致性問題(越基礎(chǔ)的類由越上層的加載器進(jìn)行加載),基礎(chǔ)類型之所以被稱為“基礎(chǔ)”,是因?yàn)樗鼈兛偸亲鳛楸挥脩舸a繼承、調(diào)用的API存在,但程序設(shè)計(jì)往往沒有絕對(duì)不變的完美規(guī)則,如果有基礎(chǔ)類型又要調(diào)用回用戶的代碼,那該怎么辦呢?
這并非是不可能出現(xiàn)的事情,一個(gè)典型的例子便是JNDI服務(wù),JNDI現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動(dòng)類加載器來完成加載(在JDK 1.3時(shí)加入到rt.jar的),肯定屬于Java中很基礎(chǔ)的類型了。但JNDI存在的目的就是對(duì)資源進(jìn)行查找和集中管理,它需要調(diào)用由其他廠商實(shí)現(xiàn)并部署在應(yīng)用程序的ClassPath下的JNDI服務(wù)提供者接口(Service Provider Interface,SPI)的代碼,現(xiàn)在問題來了,啟動(dòng)類加載器是絕不可能認(rèn)識(shí)、加載這些代碼的,那該怎么辦?
為了解決這個(gè)困境,Java的設(shè)計(jì)團(tuán)隊(duì)只好引入了一個(gè)不太優(yōu)雅的設(shè)計(jì):線程上下文類加載器(Thread Context ClassLoader)。這個(gè)類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程時(shí)還未設(shè)置,它將會(huì)從父線程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。有了線程上下文類加載器,程序就可以做一些“舞弊”的事情了。JNDI服務(wù)使用這個(gè)線程上下文類加載器去加載所需的SPI服務(wù)代碼,這是一種父類加載器去請(qǐng)求子類加載器完成類加載的行為,這種行為實(shí)際上是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,已經(jīng)違背了雙親委派模型的一般性原則,但也是無可奈何的事情。Java中涉及SPI的加載基本上都采用這種方式來完成,例如JNDI、JDBC、JCE、JAXB和JBI等。
不過,當(dāng)SPI的服務(wù)提供者多于一個(gè)的時(shí)候,代碼就只能根據(jù)具體提供者的類型來硬編碼判斷,為了消除這種極不優(yōu)雅的實(shí)現(xiàn)方式,在JDK 6時(shí),JDK提供了java.util.ServiceLoader類,以META-INF/services中的配置信息,輔以責(zé)任鏈模式,這才算是給SPI的加載提供了一種相對(duì)合理的解決方案。
雙親委派模型的第三次“被破壞”是由于用戶對(duì)程序動(dòng)態(tài)性的追求而導(dǎo)致的,這里所說的“動(dòng)態(tài)性”指的是一些非常“熱”門的名詞:代碼熱替換(Hot Swap)、模塊熱部署(Hot Deployment)等。說白了就是希望Java應(yīng)用程序能像我們的電腦外設(shè)那樣,接上鼠標(biāo)、U盤,不用重啟機(jī)器就能立即使用,鼠標(biāo)有問題或要升級(jí)就換個(gè)鼠標(biāo),不用關(guān)機(jī)也不用重啟。對(duì)于個(gè)人電腦來說,重啟一次其實(shí)沒有什么大不了的,但對(duì)于一些生產(chǎn)系統(tǒng)來說,關(guān)機(jī)重啟一次可能就要被列為生產(chǎn)事故,這種情況下熱部署就對(duì)軟件開發(fā)者,尤其是大型系統(tǒng)或企業(yè)級(jí)軟件開發(fā)者具有很大的吸引力。
雖然這里使用了“被破壞”這個(gè)詞來形容上述不符合雙親委派模型原則的行為,但這里“被破壞”并不一定是帶有貶義的。只要有明確的目的和充分的理由,突破舊有原則無疑是一種創(chuàng)新。正如OSGi中的類加載器的設(shè)計(jì)不符合傳統(tǒng)的雙親委派的類加載器架構(gòu),且業(yè)界對(duì)其為了實(shí)現(xiàn)熱部署而帶來的額外的高復(fù)雜度還存在不少爭(zhēng)議,但對(duì)這方面有了解的技術(shù)人員基本還是能達(dá)成一個(gè)共識(shí),認(rèn)為OSGi中對(duì)類加載器的運(yùn)用是值得學(xué)習(xí)的,完全弄懂了OSGi的實(shí)現(xiàn),就算是掌握了類加載器的精粹。
自定義類加載器示例:自定義類加載器只需要繼承 java.lang.ClassLoader 類,該類有兩個(gè)核心方法:
所以我們自定義類加載器主要是重寫findClass方法,但是如果我們不想用雙親委派機(jī)制,其實(shí)可以通過重寫loadClass實(shí)現(xiàn)。
public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("http://.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); //defineClass將一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class對(duì)象,這個(gè)字節(jié)數(shù)組是class文件讀取后最終的字節(jié)數(shù)組。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } } public static void main(String args[]) throws Exception { //初始化自定義類加載器,會(huì)先初始化父類ClassLoader,其中會(huì)把自定義類加載器的父加載器設(shè)置為應(yīng)用程序類加載器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/test"); //D盤創(chuàng)建 test/com/tuling/jvm 幾級(jí)目錄,將User類的復(fù)制類User1.class丟入該目錄 Class clazz = classLoader.loadClass("com.tuling.jvm.User1"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); }}運(yùn)行結(jié)果:=======自己的加載器加載類調(diào)用方法=======
打破雙親委派機(jī)制 再來一個(gè)沙箱安全機(jī)制示例,嘗試打破雙親委派機(jī)制,用自定義類加載器加載我們自己實(shí)現(xiàn)的 java.lang.String.class
public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("http://.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } /** * 重寫類加載方法,實(shí)現(xiàn)自己的加載邏輯,不委派給雙親加載 * @param name * @param resolve * @return * @throws ClassNotFoundException */ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } } public static void main(String args[]) throws Exception { MyClassLoader classLoader = new MyClassLoader("D:/test"); //嘗試用自己改寫類加載機(jī)制去加載自己寫的java.lang.String.class Class clazz = classLoader.loadClass("java.lang.String"); Object obj = clazz.newInstance(); Method method= clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); }}運(yùn)行結(jié)果:java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
在第六步中jvm只是為了找到將要加載類的類加載器,之后便要開始真正的加載邏輯,整個(gè)加載的過程如下圖:
加載 >> 驗(yàn)證 >> 準(zhǔn)備 >> 解析 >> 初始化 >> 使用 >> 卸載
加載是整個(gè)類加載過程的一個(gè)階段,本階段Java虛擬機(jī)規(guī)定需要完成以下三件事情。
《Java虛擬機(jī)規(guī)范》對(duì)這三點(diǎn)要求其實(shí)并不是特別具體,留給虛擬機(jī)實(shí)現(xiàn)與Java應(yīng)用的靈活度都是 相當(dāng)大的。例如“通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流”這條規(guī)則,它并沒有指明二進(jìn)制字節(jié)流必須得從某個(gè)Class文件中獲取,確切地說是根本沒有指明要從哪里獲取、如何獲取。僅僅這一點(diǎn)空隙,Java虛擬機(jī)的使用者們就可以在加載階段搭構(gòu)建出一個(gè)相當(dāng)開放廣闊的舞臺(tái),例如:
相對(duì)于類加載過程的其他階段,非數(shù)組類型的加載階段(準(zhǔn)確地說,是加載階段中獲取類的二進(jìn) 制字節(jié)流的動(dòng)作)是開發(fā)人員可控性最強(qiáng)的階段。加載階段既可以使用Java虛擬機(jī)里內(nèi)置的引導(dǎo)類加 載器來完成,也可以由用戶自定義的類加載器去完成,開發(fā)人員通過定義自己的類加載器去控制字節(jié) 流的獲取方式(重寫一個(gè)類加載器的findClass()或loadClass()方法),實(shí)現(xiàn)根據(jù)自己的想法來賦予應(yīng)用 程序獲取運(yùn)行代碼的動(dòng)態(tài)性。
對(duì)于數(shù)組類而言,情況就有所不同,數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機(jī)直接在 內(nèi)存中動(dòng)態(tài)構(gòu)造出來的。但數(shù)組類與類加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類的元素類型(Element Type,指的是數(shù)組去掉所有維度的類型)最終還是要靠類加載器來完成加載,一個(gè)數(shù)組類(下面簡(jiǎn)稱 為C)創(chuàng)建過程遵循以下規(guī)則:
·如果數(shù)組的組件類型(Component Type,指的是數(shù)組去掉一個(gè)維度的類型,注意和前面的元素類 型區(qū)分開來)是引用類型,那就遞歸采用本節(jié)中定義的加載過程去加載這個(gè)組件類型,數(shù)組C將被標(biāo) 識(shí)在加載該組件類型的類加載器的類名稱空間上(這點(diǎn)很重要,在7.4節(jié)會(huì)介紹,一個(gè)類型必須與類加 載器一起確定唯一性)。·如果數(shù)組的組件類型不是引用類型(例如int[]數(shù)組的組件類型為int),Java虛擬機(jī)將會(huì)把數(shù)組C 標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)。·數(shù)組類的可訪問性與它的組件類型的可訪問性一致,如果組件類型不是引用類型,它的數(shù)組類的 可訪問性將默認(rèn)為public,可被所有的類和接口訪問到。
加載階段結(jié)束后,Java虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所設(shè)定的格式存儲(chǔ)在方法區(qū)之中 了,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式完全由虛擬機(jī)實(shí)現(xiàn)自行定義,《Java虛擬機(jī)規(guī)范》未規(guī)定此區(qū)域的具體 數(shù)據(jù)結(jié)構(gòu)。類型數(shù)據(jù)妥善安置在方法區(qū)之后,會(huì)在Java堆內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對(duì)象, 這個(gè)對(duì)象將作為程序訪問方法區(qū)中的類型數(shù)據(jù)的外部接口。加載階段與連接階段的部分動(dòng)作(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段 尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進(jìn)行的動(dòng)作,仍然屬于連接階段的一部 分,這兩個(gè)階段的開始時(shí)間仍然保持著固定的先后順序。
驗(yàn)證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛 擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。
java本身是一個(gè)安全語言,使用java編寫的代碼不會(huì)出現(xiàn)像數(shù)組越界這樣的錯(cuò)誤。因?yàn)閖avac在編譯的時(shí)候就會(huì)做很多相關(guān)的校驗(yàn),諸如上面說的錯(cuò)誤,在編譯環(huán)節(jié)就已經(jīng)被暴露出來,對(duì)于java語言編譯的字節(jié)碼文件對(duì)于虛擬機(jī)來說是安全的,但是我們知道java虛擬機(jī)是一個(gè)可以運(yùn)行多種語言的平臺(tái),它接受任何語言編譯成的字節(jié)碼文件,上面加載階段也講了,字節(jié)碼文件的來源比較多,字節(jié)碼的合法性等也無法保證,Java虛擬機(jī)如果不檢查輸入的字節(jié)流,對(duì)其完全信任的話,很可能會(huì)因?yàn)檩d入了有錯(cuò)誤或有惡意企圖的字節(jié)碼流而導(dǎo)致整個(gè)系統(tǒng)受攻擊甚至崩潰,所以驗(yàn)證字節(jié)碼是Java虛擬機(jī)保護(hù)自身的一項(xiàng)必要措施。
驗(yàn)證階段是非常重要的,這個(gè)階段是否嚴(yán)謹(jǐn),直接決定了Java虛擬機(jī)是否能承受惡意代碼的攻擊,從代碼量和耗費(fèi)的執(zhí)行性能的角度上講,驗(yàn)證階段的工作量在虛擬機(jī)的類加載過程中占了相當(dāng)大的比重。但是早期版本的java虛擬機(jī)對(duì)這個(gè)階段檢查比較模糊和籠統(tǒng),并未確切說明。直到第7版的java虛擬機(jī)規(guī)范才變的具體起來。規(guī)范中大體把此階段分為4個(gè)動(dòng)作進(jìn)行校驗(yàn):文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。
(1) 文件格式驗(yàn)證驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。這一階段可能包括下面這些驗(yàn)證點(diǎn):
實(shí)際上第一階段的驗(yàn)證點(diǎn)還遠(yuǎn)不止這些,上面所列的只是從HotSpot虛擬機(jī)源碼[1]中摘抄的一小部分內(nèi)容,該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類型信息的要求。這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過了這個(gè)階段的驗(yàn)證之后,這段字節(jié)流才被允許進(jìn)入Java虛擬機(jī)內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面的三個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)上進(jìn)行的,不會(huì)再直接讀取、操作字節(jié)流了。
(2)元數(shù)據(jù)驗(yàn)證對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合《Java語言規(guī)范》的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下:
這個(gè)過程主要是對(duì)類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在與《Java語言規(guī)范》定義相悖的元數(shù)據(jù)信息。
(3) 字節(jié)碼驗(yàn)證第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗(yàn)完畢以后,這階段就要對(duì)類的方法體(Class文件中的Code屬性)進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為,例如:
由于數(shù)據(jù)流分析和控制流分析的高度復(fù)雜性,Java虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)為了避免過多的執(zhí)行時(shí)間消耗在字節(jié)碼驗(yàn)證階段中,在JDK 6之后的Javac編譯器和Java虛擬機(jī)里進(jìn)行了一項(xiàng)聯(lián)合優(yōu)化,把盡可能多的校驗(yàn)輔助措施挪到Javac編譯器里進(jìn)行。
具體做法是給方法體Code屬性的屬性表中新增加了一項(xiàng)名為“StackMapTable”的新屬性,這項(xiàng)屬性描述了方法體所有的基本塊(Basic Block,指按照控制流拆分的代碼塊)開始時(shí)本地變量表和操作棧應(yīng)有的狀態(tài),在字節(jié)碼驗(yàn)證期間,Java虛擬機(jī)就不需要根據(jù)程序推導(dǎo)這些狀態(tài)的合法性,只需要檢查StackMapTable屬性中的記錄是否合法即可。這樣就將字節(jié)碼驗(yàn)證的類型推導(dǎo)轉(zhuǎn)變?yōu)轭愋蜋z查,從而節(jié)省了大量校驗(yàn)時(shí)間。
理論上StackMapTable屬性也存在錯(cuò)誤或被篡改的可能,所以是否有可能在惡意篡改了Code屬性的同時(shí),也生成相應(yīng)的StackMapTable屬性來騙過虛擬機(jī)的類型校驗(yàn),則是虛擬機(jī)設(shè)計(jì)者們需要仔細(xì)思考的問題。
JDK 6的HotSpot虛擬機(jī)中提供了-XX:-UseSplitVerifier選項(xiàng)來關(guān)閉掉這項(xiàng)優(yōu)化,或者使用參數(shù)XX:+FailOverToOldVerifier要求在類型校驗(yàn)失敗的時(shí)候退回到舊的類型推導(dǎo)方式進(jìn)行校驗(yàn)。而到了JDK 7之后,盡管虛擬機(jī)中仍然保留著類型推導(dǎo)驗(yàn)證器的代碼,但是對(duì)于主版本號(hào)大于50(對(duì)應(yīng)JDK6)的Class文件,使用類型檢查來完成數(shù)據(jù)流分析校驗(yàn)則是唯一的選擇,不允許再退回到原來的類型推導(dǎo)的校驗(yàn)方式。
(4) 符號(hào)引用驗(yàn)證最后一個(gè)階段的校驗(yàn)行為發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用[3]的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。
符號(hào)引用驗(yàn)證可以看作是對(duì)類自身以外(常量池中的各種符號(hào)引用)的各類信息進(jìn)行匹配性校驗(yàn),通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、字段等資源。本階段通常需要校驗(yàn)下列內(nèi)容:
符號(hào)引用驗(yàn)證的主要目的是確保解析行為能正常執(zhí)行,如果無法通過符號(hào)引用驗(yàn)證,Java虛擬機(jī)將會(huì)拋出一個(gè)java.lang.IncompatibleClassChangeError的子類異常,典型的如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
驗(yàn)證階段對(duì)于虛擬機(jī)的類加載機(jī)制來說,是一個(gè)非常重要的、但卻不是必須要執(zhí)行的階段,因?yàn)轵?yàn)證階段只有通過或者不通過的差別,只要通過了驗(yàn)證,其后就對(duì)程序運(yùn)行期沒有任何影響了。如果程序運(yùn)行的全部代碼(包括自己編寫的、第三方包中的、從外部加載的、動(dòng)態(tài)生成的等所有代碼)都已經(jīng)被反復(fù)使用和驗(yàn)證過,在生產(chǎn)環(huán)境的實(shí)施階段就可以考慮使用-Xverify:none參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。
準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初 始值的階段,從概念上講,這些變量所使用的內(nèi)存都應(yīng)當(dāng)在方法區(qū)中進(jìn)行分配,但必須注意到方法區(qū) 本身是一個(gè)邏輯上的區(qū)域,在JDK 7及之前,HotSpot使用永久代來實(shí)現(xiàn)方法區(qū)時(shí),實(shí)現(xiàn)是完全符合這 種邏輯概念的;而在JDK 8及之后,類變量則會(huì)隨著Class對(duì)象一起存放在Java堆中,這時(shí)候“類變量在 方法區(qū)”就完全是一種對(duì)邏輯概念的表述了,關(guān)于這部分內(nèi)容,筆者已在4.3.1節(jié)介紹并且驗(yàn)證過。
關(guān)于準(zhǔn)備階段,還有兩個(gè)容易產(chǎn)生混淆的概念筆者需要著重強(qiáng)調(diào),首先是這時(shí)候進(jìn)行內(nèi)存分配的 僅包括類變量,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。其 次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量的定義為:
public static int value = 123;
那變量value在準(zhǔn)備階段過后的初始值為0而不是123,因?yàn)檫@時(shí)尚未開始執(zhí)行任何Java方法,而把 value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器()方法之中,所以把value賦值 為123的動(dòng)作要到類的初始化階段才會(huì)被執(zhí)行。
Java中所有基本數(shù)據(jù)類型的零值如下:
上面提到在“通常情況”下初始值是零值,那言外之意是相對(duì)的會(huì)有某些“特殊情況”:如果類字段 的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量值就會(huì)被初始化為ConstantValue屬性所指定 的初始值,假設(shè)上面類變量value的定義修改為:
public static final int value = 123;
編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)Con-stantValue的設(shè)置 將value賦值為123。
解析階段做的事情其實(shí)就是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程。
(1) java虛擬機(jī)什么時(shí)候開始解析?
《Java虛擬機(jī)規(guī)范》中只要求了在執(zhí)行ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic這17個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)它們所使用的符號(hào)引用進(jìn)行解析。
我們知道類加載過程中的這幾個(gè)步驟,只要求發(fā)生的先后順序,并未要求具體的放生時(shí)間,因此解析階段可以發(fā)生在類加載器加載的階段,也可以發(fā)生在字節(jié)碼被使用的時(shí)候,而且jvm中類的加載本身就是懶加載。
(2) java虛擬機(jī)對(duì)什么內(nèi)容進(jìn)行解析?
解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符這7類符號(hào)引用進(jìn)行,分別對(duì)應(yīng)于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8種常量類型,類似地,對(duì)方法或者字段的訪問,也會(huì)在解析階段中對(duì)它們的可訪問性(public、protected、private、)進(jìn)行檢查。
(3) java虛擬機(jī)解析內(nèi)容可否復(fù)用?
對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見的事情,除invokedynamic指令以外,虛擬機(jī)實(shí)現(xiàn)可以對(duì)第一次解析的結(jié)果進(jìn)行緩存,譬如在運(yùn)行時(shí)直接引用常量池中的記錄,并把常量標(biāo)識(shí)為已解析狀態(tài),從而避免解析動(dòng)作重復(fù)進(jìn)行。無論是否真正執(zhí)行了多次解析動(dòng)作,Java虛擬機(jī)都需要保證的是在同一個(gè)實(shí)體中,如果一個(gè)符號(hào)引用之前已經(jīng)被成功解析過,那么后續(xù)的引用解析請(qǐng)求就應(yīng)當(dāng)一直能夠成功;同樣地,如果第一次解析失敗了,其他指令對(duì)這個(gè)符號(hào)的解析請(qǐng)求也應(yīng)該收到相同的異常,哪怕這個(gè)請(qǐng)求的符號(hào)在后來已成功加載進(jìn)Java虛擬機(jī)內(nèi)存之中。不過對(duì)于invokedynamic指令,上面的規(guī)則就不成立了。當(dāng)碰到某個(gè)前面已經(jīng)由invokedynamic指令觸發(fā)過解析的符號(hào)引用時(shí),并不意味著這個(gè)解析結(jié)果對(duì)于其他invokedynamic指令也同樣生效。因?yàn)閕nvokedynamic指令的目的本來就是用于動(dòng)態(tài)語言支持,它對(duì)應(yīng)的引用稱為“動(dòng)態(tài)調(diào)用點(diǎn)限定符”,這里“動(dòng)態(tài)”的含義是指必須等到程序?qū)嶋H運(yùn)行到這條指令時(shí),解析動(dòng)作才能進(jìn)行。相對(duì)地,其余可觸發(fā)解析的指令都是“靜態(tài)”的,可以在剛剛完成加載階段,還沒有開始執(zhí)行代碼時(shí)就提前進(jìn)行解析。
類的初始化階段是類加載過程的最后一個(gè)步驟,之前介紹的幾個(gè)類加載的動(dòng)作里,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來主導(dǎo)控制。直到初始化階段,Java虛擬機(jī)才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。
進(jìn)行準(zhǔn)備階段時(shí),變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會(huì)根據(jù)程序員通過程序編碼制定的主觀計(jì)劃去初始化類變量和其他資源。我們也可以從另外一種更直接的形式來表達(dá):初始化階段就是執(zhí)行類構(gòu)造器clinit方法的過程。clinit并不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動(dòng)生成物,但我們非常有必要了解這個(gè)方法具體是如何產(chǎn)生的,以及 clinit方法執(zhí)行過程中各種可能會(huì)影響程序運(yùn)行行為的細(xì)節(jié),這部分比起其他類加載過程更貼近于普通的程序開發(fā)人員的實(shí)際工作。
clinit方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問。
public class Test { static { i = 0; // 給變量復(fù)制可以正常編譯通過 System.out.print(i); // 這句編譯器會(huì)提示“非法向前引用” } static int i = 1; }
clinit方法與類的構(gòu)造函數(shù)(即在虛擬機(jī)視角中的實(shí)例構(gòu)造器init方法)不同,它不需要顯式地調(diào)用父類構(gòu)造器,Java虛擬機(jī)會(huì)保證在子類的clinit方法執(zhí)行前,父類的clinit方法已經(jīng)執(zhí)行完畢。因此在Java虛擬機(jī)中第一個(gè)被執(zhí)行的clinit方法的類型肯定是java.lang.Object。由于父類的clinit方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值 操作,下面代碼中字段B的值將會(huì)是2而不是1。
static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); }
clinit方法對(duì)于類或接口來說并不是必需的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成clinit方法。
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(huì)生成clinit方法。但接口與類不同的是,執(zhí)行接口的clinit方法不需要先執(zhí)行父接口的clinit方法, 因?yàn)橹挥挟?dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。此外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的clinit方法。
Java虛擬機(jī)必須保證一個(gè)類的clinit方法在多線程環(huán)境中被正確地加鎖同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有其中一個(gè)線程去執(zhí)行這個(gè)類的clinit方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行完畢clinit方法。如果在一個(gè)類的clinit方法中有耗時(shí)很長(zhǎng)的操作,那就可能造成多個(gè)進(jìn)程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的,如下面代碼
static class DeadLoopClass { static { // 如果不加上這個(gè)if語句,編譯器將提示“Initializer does not complete normally” 并拒絕編譯 if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { } } } } public static void main(String[] args) { Runnable script = new Runnable() { public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + " run over"); } }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); }
運(yùn)行結(jié)果如下,一條線程在死循環(huán)以模擬長(zhǎng)時(shí)間操作,另外一條線程在阻塞等待:
Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-0,5,main]init DeadLoopClass
上面是從整體上闡述了類加載的整個(gè)過程,重點(diǎn)是加載的每個(gè)階段都不能少,存在先后順序要求,但是具體執(zhí)行時(shí)間不確定,需要注意的是類加載器在整個(gè)類加載過程中做的事情僅僅是“通過一個(gè)類的全限定名來獲取描述該類的二進(jìn)制字節(jié)流”。
本文鏈接:http://www.tebozhan.com/showinfo-26-14144-0.htmlJVM類加載器就做了這么點(diǎn)事?
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com