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

被問到JVM類加載機(jī)制中雙親委派模型是什么,三次被破壞指什么?

開發(fā) 前端
雙親委派模型并不是一個(gè)具有強(qiáng)制性約束的模型,而是Java設(shè)計(jì)者推薦給開發(fā)者們的類加載器實(shí)現(xiàn)方式。在Java的世界中大部分的類加載器都遵循這個(gè)模型,但也有例外的情況,直到Java模塊化出現(xiàn)為止,雙親委派模型主要出現(xiàn)過3次較大規(guī)?!氨黄茐摹钡那闆r。

本文就通篇來講解下jvm類加載機(jī)制究竟有哪些內(nèi)容需要我們掌握。

假設(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;
    }
}
  • 其中的靜態(tài)變量保證加載完就生成了Launcher實(shí)例;
  • 其中的構(gòu)造方法中最終生成了ExtClassLoader(擴(kuò)展類加載器)和AppClassLoader(應(yīng)用類加載器),并且把AppClassLoader賦值給Launcher實(shí)例的成員變量loader;
  • 提供getClassLoader成員方法,可以獲取到成員變量loader;
  • 提供一個(gè)靜態(tài)getLauncher方法獲取當(dāng)前生成的這個(gè)Launcher實(shí)例

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)

1.第6點(diǎn)調(diào)用的方法主要是選擇由哪個(gè)類加載器進(jìn)行加載,講到這里我們呢就先了解下jvm中的類加載器

Java中類加載器分類

  • 引導(dǎo)類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的核心類庫(kù),比如rt.jar、charsets.jar等
  • 擴(kuò)展類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的ext擴(kuò)展目錄中的JAR類包
  • 應(yīng)用程序類加載器:負(fù)責(zé)加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類
  • 自定義加載器:負(fù)責(zé)加載用戶自定義路徑下的類包
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é)果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@3764951d
the appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dc

bootstrapLoader加載以下文件:
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/classes

extClassloader加載以下文件:
D:\dev\Java\jdk1.8.0_45\jre\lib\ext;C:\Windows\Sun\Java\lib\ext

appClassLoader加載以下文件:
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è)路徑下的類。

雙親委派機(jī)制

上面的步驟中提過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ī)制?

  • 沙箱安全機(jī)制:自己寫的java.lang.String.class類不會(huì)被加載,這樣便可以防止核心API庫(kù)被隨意篡改
  • 避免類的重復(fù)加載:當(dāng)父親已經(jīng)加載了該類時(shí),就沒有必要在ClassLoader再加載一次,保證被加載類的唯一性
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ī)?!氨黄茐摹钡那闆r。

雙親委派模型的第一次“被破壞”其實(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é),為了兼容這些已有代碼,無(wú)法再以技術(shù)手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個(gè)新的protected方法findClass(),并引導(dǎo)用戶編寫的類加載邏輯時(shí)盡可能去重寫這個(gè)方法,而不是在loadClass()中編寫代碼。上節(jié)我們已經(jīng)分析過loadClass()方法,雙親委派的具體邏輯就實(shí)現(xiàn)在這里面,按照l(shuí)oadClass()方法的邏輯,如果父類加載失敗,會(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)違背了雙親委派模型的一般性原則,但也是無(wú)可奈何的事情。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è)詞來形容上述不符合雙親委派模型原則的行為,但這里“被破壞”并不一定是帶有貶義的。只要有明確的目的和充分的理由,突破舊有原則無(wú)疑是一種創(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è)核心方法:

  • 一個(gè)是loadClass(String, boolean),實(shí)現(xiàn)了雙親委派機(jī)制;
  • 一個(gè)方法是findClass,默認(rèn)實(shí)現(xiàn)是空方法。

所以我們自定義類加載器主要是重寫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("\\.", "/");
            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("\\.", "/");
            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)

2.第7點(diǎn)調(diào)用上層的findClass方法,進(jìn)入類加載環(huán)節(jié)做了什么事情

在第六步中jvm只是為了找到將要加載類的類加載器,之后便要開始真正的加載邏輯,整個(gè)加載的過程如下圖:

圖片圖片

加載 >> 驗(yàn)證 >> 準(zhǔn)備 >> 解析 >> 初始化 >> 使用 >> 卸載

加載

加載是整個(gè)類加載過程的一個(gè)階段,本階段Java虛擬機(jī)規(guī)定需要完成以下三件事情。

  • 通過一個(gè)類的全限定名來獲取定義此類的二進(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è)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入 口。

《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),例如:

  • 從ZIP壓縮包中讀取,這很常見,最終成為日后JAR、EAR、WAR格式的基礎(chǔ)。
  • 從網(wǎng)絡(luò)中獲取,這種場(chǎng)景最典型的應(yīng)用就是Web Applet。
  • 運(yùn)行時(shí)計(jì)算生成,這種場(chǎng)景使用得最多的就是動(dòng)態(tài)代理技術(shù),在java.lang.reflect.Proxy中,就是用 了ProxyGenerator.generateProxyClass()來為特定接口生成形式為“*$Proxy”的代理類的二進(jìn)制字節(jié)流。
  • 由其他文件生成,典型場(chǎng)景是JSP應(yīng)用,由JSP文件生成對(duì)應(yīng)的Class文件。
  • 從數(shù)據(jù)庫(kù)中讀取,這種場(chǎng)景相對(duì)少見些,例如有些中間件服務(wù)器(如SAP Netweaver)可以選擇 把程序安裝到數(shù)據(jù)庫(kù)中來完成程序代碼在集群間的分發(fā)。
  • 可以從加密文件中獲取,這是典型的防Class文件被反編譯的保護(hù)措施,通過加載時(shí)解密Class文件來保障程序運(yùn)行邏輯不被窺探。

相對(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)證

驗(yàn)證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛 擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。

java本身是一個(gè)安全語(yǔ)言,使用java編寫的代碼不會(huì)出現(xiàn)像數(shù)組越界這樣的錯(cuò)誤。因?yàn)閖avac在編譯的時(shí)候就會(huì)做很多相關(guān)的校驗(yàn),諸如上面說的錯(cuò)誤,在編譯環(huán)節(jié)就已經(jīng)被暴露出來,對(duì)于java語(yǔ)言編譯的字節(jié)碼文件對(duì)于虛擬機(jī)來說是安全的,但是我們知道java虛擬機(jī)是一個(gè)可以運(yùn)行多種語(yǔ)言的平臺(tái),它接受任何語(yǔ)言編譯成的字節(jié)碼文件,上面加載階段也講了,字節(jié)碼文件的來源比較多,字節(jié)碼的合法性等也無(wú)法保證,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ù)0xCAFEBABE開頭。
  • 主、次版本號(hào)是否在當(dāng)前Java虛擬機(jī)接受范圍之內(nèi)。
  • 常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志)。
  • 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數(shù)據(jù)。
  • Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息。

實(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)行語(yǔ)義分析,以保證其描述的信息符合《Java語(yǔ)言規(guī)范》的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下:

  • 這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)。
  • 這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
  • 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法。
  • 類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載,例如方法參數(shù)都一致,但返回值類型卻不同等)。

這個(gè)過程主要是對(duì)類的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),保證不存在與《Java語(yǔ)言規(guī)范》定義相悖的元數(shù)據(jù)信息。

3.字節(jié)碼驗(yàn)證第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗(yàn)完畢以后,這階段就要對(duì)類的方法體(Class文件中的Code屬性)進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為,例如:·保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)類似于“在操作棧放置了一個(gè)int類型的數(shù)據(jù),使用時(shí)卻按long類型來加載入本地變量表中”這樣的情況?!けWC任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。·保證方法體中的類型轉(zhuǎn)換總是有效的,例如可以把一個(gè)子類對(duì)象賦值給父類數(shù)據(jù)類型,這是安全的,但是把父類對(duì)象賦值給子類數(shù)據(jù)類型,甚至把對(duì)象賦值給與它毫無(wú)繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不合法的。

由于數(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)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類?!ぴ谥付愔惺欠翊嬖诜戏椒ǖ淖侄蚊枋龇昂?jiǎn)單名稱所描述的方法和字段。·符號(hào)引用中的類、字段、方法的可訪問性是否可被當(dāng)前類訪問。

符號(hào)引用驗(yàn)證的主要目的是確保解析行為能正常執(zhí)行,如果無(wú)法通過符號(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)備

準(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)引用替換為直接引用的過程。

  • 符號(hào)引用:
    符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定是已經(jīng)加載到虛擬機(jī)內(nèi)存當(dāng)中的內(nèi)容。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號(hào)引用必須都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在《Java虛擬機(jī)規(guī)范》的Class文件格式中。
  • 直接引用:
    直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī)的內(nèi)存中存在。

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中類的加載本身就是懶加載。

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)行檢查。

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)行。無(wú)論是否真正執(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)語(yǔ)言支持,它對(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)語(yǔ)句塊(static{}塊)中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序決定的,靜態(tài)語(yǔ)句塊中只能訪問到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問。

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ǔ)句塊要優(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)語(yǔ)句塊,也沒有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成clinit方法。

接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(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語(yǔ)句,編譯器將提示“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

3.總結(jié)

上面是從整體上闡述了類加載的整個(gè)過程,重點(diǎn)是加載的每個(gè)階段都不能少,存在先后順序要求,但是具體執(zhí)行時(shí)間不確定,需要注意的是類加載器在整個(gè)類加載過程中做的事情僅僅是“通過一個(gè)類的全限定名來獲取描述該類的二進(jìn)制字節(jié)流”。

責(zé)任編輯:武曉燕 來源: 碼農(nóng)本農(nóng)
相關(guān)推薦

2021-05-12 16:27:55

Java雙親模型

2023-12-06 12:11:43

類加載器雙親委派模型

2024-12-04 09:01:55

引導(dǎo)類加載器C++

2023-08-04 08:53:42

2023-02-03 07:24:49

雙親委派模型

2012-08-03 09:14:23

2024-06-24 08:24:57

2024-03-12 07:44:53

JVM雙親委托機(jī)制類加載器

2021-03-01 08:54:39

開發(fā)雙親委派

2024-12-02 09:01:23

Java虛擬機(jī)內(nèi)存

2024-03-06 08:00:56

javaAQS原生

2023-10-31 16:00:51

類加載機(jī)制Java

2024-07-05 09:31:37

2021-01-06 09:51:19

類加載器雙親委派模型

2023-09-07 16:46:54

TCP數(shù)據(jù)傳遞

2022-12-13 18:09:25

連接狀態(tài)客戶端

2020-11-06 00:50:16

JavaClassLoaderJVM

2023-10-30 01:02:56

Java類類加載器雙親委派

2022-07-25 07:07:35

TCP客戶端服務(wù)器

2019-12-12 10:36:43

TCPSYNIP
點(diǎn)贊
收藏

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