《深刻理解Java虛擬機》讀書筆記六

第七章 虛擬機類加載機制
html

一、類加載的時機java

虛擬機的類加載機制:程序員

  • 虛擬機把描述類的數據從class文件中加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成了能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
  • 類從被加載到虛擬機內存中開始到卸載出內存爲止,他的整個生命週期包括加載、驗證、準備、解析、初始化、使用和卸載七個階段。
  • 加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類加載過程必須按照這種順序循序漸進的開始。而解析階段不必定在某種狀況下,能夠在初始化階段以後再開始,這是爲了支持Java運行時綁定。

類初始化的五種狀況:數組

  • 遇到new、getstatic、putstatic和invokestatic這4條指令時,若是類沒有初始化時,則須要先觸發其初始化。生成這4條指令的最多見Java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外),以及調用一個類的靜態方法。
  • 使用java.lang.reflect包的方法對類進行反射調用時候。
  • 當初始化一個類時,發現其父類沒有進行初始化,則需先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶須要制定一個要執行的主類(包含main()方法的那個類),當前類須要先初始化
  • 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_putStatic、REF_getStatic、REF_invokeStatic的方法句柄,而且此方法句柄所對應的類沒有進行初始化時,須要初始化該類。

被動引用:安全

  • 這五種狀況被稱爲對一個類的主動引用,除此以外全部的引用類的方式都不會觸發初始化,稱爲被動引用。
  • 經過子類引用父類的靜態字段不會致使初始化。對於靜態變量只有直接定義這個類的字段纔會被初始化所以經過子類來引用父類中定義的靜態變量不會致使子類的初始化。

    父類:數據結構

    package com.ecut.classload;
    
    public class SuperClass{
        static {
            System.out.println("super init!");
        }
    
        static int value = 1;
    }

    子類:多線程

    package com.ecut.classload; public class SubClass extends SuperClass{ static { System.out.println("sub init!"); } }
    package com.ecut.classload;
    
    /**
     * 經過子類引用父類的靜態字段,不會致使初始化
     */
    public class NotInitialization1 {
        public static void main(String[] args) {
            System.out.println(SubClass.value);
        }
    }

    運行結果以下:jvm

    ........
    [Loaded com.ecut.classload.SuperClass from file:/D:/code/java/jvm-test/out/production/jvm-test/] [Loaded com.ecut.classload.SubClass from file:/D:/code/java/jvm-test/out/production/jvm-test/] super init! 1
    .......
  • 經過數組定義來引用類,不會觸發此類的初始化
    package com.ecut.classload;
    
    /**
     * 經過數組定義來引用類,不會觸發此類的初始化
     * -XX:+TraceClassLoading 
     */
    public class NotInitializatio2 {
        public static void main(String[] args) {
            SuperClass[] superClasses  = new SuperClass[10];
        }
    }

    運行結果以下:ide

    .......
    [Loaded java.net.Proxy$Type from C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
    [Loaded com.ecut.classload.SuperClass from file:/D:/code/java/jvm-test/out/production/jvm-test/]
    [Loaded java.util.ArrayList$Itr from C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
    [Loaded sun.net.NetHooks from C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
    [Loaded java.net.Inet6Address$Inet6AddressHolder from C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
    [Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
    [Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
    ........
  • 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義的常量的類,所以不會觸發定義常量的類初始化。

    常量類:測試

    package com.ecut.classload;
    
    public class ConstClass {
    
        public static final String HELLOWORD = "hello world";
    
        static  {
            System.out.println("const init !");
        }
    }

    測試類:

    package com.ecut.classload;
    
    /**
     * 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義的常量的類,所以不會觸發定義常量的類初始化
     */
    public class NotInitializatio3 {
        public static void main(String[] args) {
            System.out.println(ConstClass.HELLOWORD);
        }
    }

    雖然在源碼中引用了ConstClass類中的常量但其實在編譯階段已將此常量值存儲在了main方法所在的常量池中,對常量的引用裝換成了對自身常量池的引用。

  • 接口在初始化的時候並不要求其父接口所有要完成初始化,只要在真正使用到的時候纔會初始化。

二、類加載的過程

加載:

  • 經過一個類的全侷限定名獲取定義此類的二進制字節流(將字節碼文件( .class ) 讀入到 JVM 所管理的內存中)。
  • 將這個字節流鎖表明的靜態存儲結構轉換爲方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.class對象,做爲方法區這個類的各類數據的訪問入口。

驗證:

  • 驗證的目的是確保class文件的字節流中包含的信息符合當虛擬機的要求,而且不危害虛擬機自身安全。
  • 驗證從總體上來看虛擬機的驗證階段大體能夠分爲四個階段的檢驗工做,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
  • 文件格式驗證,保證輸入的字節流能正確的被解析並存儲到方法區,這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證,字節流纔會進入方法區中進行存儲。文件格式驗證例子,是不是魔數0xCAFEBABE開頭,主次版本號是否在當前虛擬機處理範圍以內,常量池的常量中是否有被支持的常量類型。
  • 元數據驗證,對字節描述信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,目的是對類的元數據信息進行語義分析校驗,保證不存在不符合Java語言規範的元數據信息,是對元數據中的數據類型作校驗。元數據驗證的例子,這個類是否有父類(除了object類都有父類),這個類是否繼承了被final修飾的類。
  • 字節碼驗證,目的是經過數據流和控制流分析肯定程序語義是否合法符合邏輯,對類的方法進行驗證。字節碼驗證例子,保證跳轉指令不會跳轉到方法之外的字節碼指令上,保證方法體類型轉換時有效。
  • 符號引用驗證,發生在將符號引用轉換成字節引用的時候,這個轉換動做將在解析的時候發生,符號引用是對常量池中各類符號引用的信息進行匹配驗證。目的是爲了解析的過程能夠正常進行。

準備:

  • 準備階段是正式爲類變量分配內存並設置類變量初始化值的階段,這些變量所使用的內存都將在方法區中進行分配。
  • 這個時候進行內存分配的變量僅包括類變量,這裏的初始值一般是數據類型的零值,除了final修飾的變量初始化值爲所給定的值。

解析:

  • 是虛擬機將常量池中符號引用替換爲直接引用的過程。解析動做主要是針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符類符號引用進行。
  • 符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,但各類虛擬機能接受的符號引用是一致的。
  • 直接引用能夠直接指向目標的指針,相對偏移量或是一個能間接定位到的句柄(對象的訪問定位有兩種,使用句柄的訪問方式和使用直接指針訪問)

初始化:

  • 初始化階段,根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源。 初始化過程是執行類構造器<client>()方法的過程。
  • 編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,而定義在它以後的變量,在前面的靜態語句塊能夠賦值,但不能訪問。
  • 在<client>()方法執行以前,父類的<client>()方法已經執行完畢,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。
  • 虛擬機會保證一個類的<client>()方法在多線程環境中被正確的加鎖、同步,若是多個線程去初始化一個類,那麼只會有一個線程去執行這個類的<client>()方法,其餘線程都須要阻塞等待。

三、類加載器

類與類加載器:

  • 虛擬機團隊把類加載階段中的經過類的全限定名獲取描述此類的二進制字節流的動做放在Java虛擬機以外來實現,以便讓應用程序本身決定如何去獲取所需類。這個實現代碼模塊稱爲類加載器。
  • 只有兩個類是有同一個類加載器,加載的前提下是有意義不然即便來源於同一個class文件被同一個虛擬機加載只要加載他們的類加載器不一樣,那這個類一定不相等。
    package com.ecut.classload;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    public class ClassLoaderTest {
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            ClassLoader myClassLoder = new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if (is == null) {
                            return super.loadClass(name);
                        }
    
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return this.defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        throw new ClassNotFoundException(name);
                    }
                }
            };
    
            Object object = myClassLoder.loadClass("com.ecut.classload.ClassLoaderTest").newInstance();
            System.out.println(object instanceof  com.ecut.classload.ClassLoaderTest);
        }
    }

    運行結果以下:

    false

類加載器的分類:

  • 啓動類加載器(根加載器,Bootstrap ClassLoader):由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中。
  • 擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的全部類庫。
  • 應用程序類加載器(系統類加載器,Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,咱們能夠直接使用這個類加載器。通常狀況,若是咱們沒有自定義類加載器默認就是用這個加載器。

雙親委派模型:

  • 雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。
  • 工做過程是若是一個類加載器收到類加載的請求,先不會本身去嘗試加載這個類,而是把這個請求委派個父類加載器,每個層級都是這樣,所以全部的加載請求最終都會送至頂層啓動類中,只有父類加載器沒法完成這個請求,纔會由子加載器嘗試去加載。
  • 做用:對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。不管哪一個一個類加載器加載object類都由根加載器完成,所以object在程序中各種加載器環境都是同一個類。
  • 雙親委派機制的是實現原理:
    public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
        
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 首先,檢查類是否已經加載。
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);//看父類加載器有沒有加載該類(父委託機制)
                        } else {
                            c = findBootstrapClassOrNull(name);//父類加載器爲空,看根加載器(Bootstrap Loader)有沒有加載
                        }
                    } catch (ClassNotFoundException e) {
    
                        //若是類沒有發現拋出ClassNotFoundException 
                    }
    
                    if (c == null) {
                        //若是仍然沒有找到,而後調用findClass爲了找到類。 
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // 這是定義類裝入器;記錄統計數據
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
        
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
        }
    

    先檢查是否被加載過,若沒有被加載過調用父類的loadclass方法,若父加載器爲空則默認使用啓動類加載器爲父加載器,若是父加載器加載失敗就拋出ClassNotFoundException ,再調用本身的findClass方法進行加載。

破壞雙親委派機制模型:

  • 第一次破壞:因爲雙親委派模型是在JDK1.2以後才被引入的,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,爲了向前兼容若是父加載器加載失敗就拋出ClassNotFoundException ,再調用本身的findClass方法進行加載。這樣就能夠保證新寫出來的類加載器符合雙親委派規則的。
  • 第二次破壞:這個模型自身的缺陷所致使的,基礎類沒法調用用戶的代碼。 爲了解決這個困境,Java設計團隊只好引入了一個不太優雅的設計:線程上下文件類加載器(Thread Context ClassLoader)。
  •  第三次破壞:因爲用戶對程序的動態性的追求致使的,例如OSGi的出現。在OSGi環境下,類加載器再也不是雙親委派模型中的樹狀結構,而是進一步發展爲網狀結構。

轉載請於明顯處標明出處

http://www.javashuo.com/article/p-kclxzdac-p.html

相關文章
相關標籤/搜索