java類加載及雙親委派機制

目錄

類加載流程 

1、加載

2、驗證

3、準備

4、解析

5、初始化

雙親委派模型

常見異常

NoClassDefFoundError

ClassNotFoundException

ClassCastException

線程上下文類加載器

JIT

編譯器

誰被編譯了?

觸發條件


當我們第一次使用該類的時候,如果該類還未被加載到內存,則系統會通過加載-連接-初始化來實現這個類的初始化。其中連接又分爲:驗證,準備和解析三步。所以一個類被加載到jvm其生命週期包括以下7個階段。類加載包括前5個階段。具體流程如下:

類加載過程
類加載流程圖

類加載流程 

      類加載器的任務就是根據一個類的全限定名來讀取此類的二進制字節流到JVM中,然後轉換爲一個與目標類對應的java.lang.Class對象實例。具體流程如下:

1、加載

  • 通過一個類的全限定名來獲取此類的二進制字節流;
  • 將字節流代表的靜態存儲結構轉換爲方法區的運行時數據結構;
  • 在內存中創建一個代表此類的java.lang.Class對象,作爲方法區此類的'各種數據的訪問入口;

2、驗證

  • 格式驗證:驗證是否符合class文件規範
  • 語義驗證:檢查一個被標記爲final的類型是否包含子類;檢查一個類中的final方法是否被子類進行重寫;確保父類和子類之間沒有不兼容的一些方法聲明(比如方法簽名相同,但方法的返回值不同)
  • 操作驗證:在操作數棧中的數據必須進行正確的操作,對常量池中的各種符號引用執行驗證(通常在解析階段執行,檢查是否可以通過符號引用中描述的全限定名定位到指定類型上,以及類成員信息的訪問修飾符是否允許訪問等)

3、準備

  • 爲類中的所有靜態變量分配內存空間,併爲其設置一個初始值(由於還沒有產生對象,實例變量不在此操作範圍內)
  • 被final修飾的static變量(常量),會直接賦值;

4、解析

  • 將常量池中的符號引用轉爲直接引用(得到類或者字段、方法在內存中的指針或者偏移量,以便直接調用該方法),這個可以在初始化之後再執行。
  • 解析需要靜態綁定的內容。靜態綁定包括一些final方法(不可以重寫),static方法(只會屬於當前類),構造器(不會被重寫)

5、初始化

  • 爲靜態變量賦值
  • 執行static代碼塊【static代碼塊只有jvm能夠調用】
  • 如果是多線程需要同時初始化一個類,僅僅只能允許其中一個線程對其執行初始化操作,其餘線程必須等待,只有在活動線程執行完對類的初始化操作之後,纔會通知正在等待的其他線程。

因爲子類存在對父類的依賴,所以類的加載順序是先加載父類後加載子類,初始化也一樣。不過,父類初始化時,子類靜態變量的值也是有的,是默認值。

  • 最終,方法區會存儲當前類類信息,包括類的靜態變量、類初始化代碼(定義靜態變量時的賦值語句 和 靜態初始化代碼塊)、實例變量定義、實例初始化代碼(定義實例變量時的賦值語句實例代碼塊和構造方法)和實例方法,還有父類的類信息引用     

類加載(初始化)時機

  1. 創建類的實例
  2. 訪問類的靜態變量或者爲靜態變量賦值
  3. 調用類的靜態方法
  4. 使用反射方法來強制創建某個類或接口對應的java.lang.Class對象
  5. 初始化某個類的子類
  6. 直接使用java.exe命令來運行某個主類

雙親委派模型

  1. 當前ClassLoader首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。每個類加載器都有自己的加載緩存,當一個類被加載了以後就會放入緩存,等下次加載的時候就可以直接返回了。
  2. 當前classLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用同樣的策略,首先查看自己的緩存,然後委託父類的父類去加載,一直到bootstrp ClassLoader.
  3. 當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。

流程如下圖所示:

這裏我們可以通過getParent()來返回該類的父類加載器。

public class ClassLoadTest {

    public static void main(String[] args) {
        try {
            System.out.println(ClassLoader.getSystemClassLoader());
            System.out.println(ClassLoader.getSystemClassLoader().getParent());
            System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

得到的結果如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@312b1dae
null

Process finished with exit code 0

正常的期望值是:系統類加載器的父類 -> 擴展類加載器 -> 啓動類加載器,但是這裏擴展類的父類不是啓動類加載器?看了下源碼:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
 
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
 
        // 首先判斷該類型是否已經被加載
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果沒被加載,就委託給父類加載或者委派給啓動類加載器加載
            try {
                if (parent != null) {
                    //如果存在父類加載器,就委派給父類加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    //如果不存在父類加載器,就通過啓動類加載器加載該類,
                    //通過調用本地方法native findBootstrapClass0(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父類加載器和啓動類加載器都不能完成加載任務,才調用自身的加載功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

通過代碼可以知道,如果發現某個類沒有被加載,且當前的父加載類爲空則會調用啓動類加載器去加載,當啓動類加載器也加載失敗的時候,纔會調用自定義的加載器。所以擴展類設置爲null和設置啓動類爲父類是一樣的,返回爲null也就正常類。

雙親委派流程

 “雙親委派”機制只是Java推薦的機制,並不是強制的機制。從虛擬機的角度來說,只存在兩種類加載器:一種是啓動類加載器(Bootstrap ClassLoader),該類加載器使用C++語言實現,屬於虛擬機自身的一部分。另外一種就是所有其它的類加載器,這些類加載器是由Java語言實現,獨立於JVM外部,並且全部繼承自抽象類java.lang.ClassLoader。

   我們可以繼承java.lang.ClassLoader類,實現自己的類加載器。如果想保持雙親委派模型,就應該重寫findClass(name)方法;如果想破壞雙親委派模型,可以重寫loadClass(name)方法

雙親委派模型的好處

  • 能夠有效確保一個類的全局唯一性;【當程序中出現多個限定名相同的類時,類加載器在執行加載時,始終只會加載其中的某一個類】
  • 避免類加載惡意代碼,信任機制,保證類 Java 核心庫的類型安全。【例如如果自己去編寫一個與rt.jar類庫中已有類重名的Java類(java.lang.Object),將會發現可以正常編譯,但永遠無法被加載運行。】
  • 分層思想

         另外:(1)父加載器中加載的類對於所有子加載器可見 (2)子類之間各自加載的類對於各自是不可間的(達到隔離效果)

常見異常

NoClassDefFoundError

       我們知道類加載器採用的是雙親委派原則,類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味着真正完成類的加載工作的類加載器和啓動這個加載過程的類加載器,可能不是同一個。真正完成類的加載工作是通過調用 defineClass來實現的;而啓動類的加載過程是通過調用 loadClass來實現的。前者稱爲一個類的定義加載器(defining loader),後者稱爲初始加載器(initiating loader)。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啓動類的加載過程並不重要,重要的是最終定義這個類的加載器。兩種類加載器的關聯之處在於:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負責啓動類 com.example.Inner的加載過程。 java.lang.ClassNotFoundException是被loadClass()拋出的, java.lang.NoClassDefFoundError是被defineClass()拋出。

      類加載器在成功加載某個類之後,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對於一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重複調用。

    loadClass()是來啓動類的加載過程的,其源代碼在前面我們已經分析過,即當父加載器不爲null同時不能完成加載請求的情況下會拋出ClassNotFoundException異常,而導致父加載器無法完成加載的一個原因很簡單就是找不到這個類,通常這種情況是傳入的類的字符串名稱書寫錯誤,如調用class.forName(String name)或者loadClass(String name)時傳入了一個錯誤的類的名稱導致類加載器無法找到這個類。

ClassNotFoundException

defineClass()是用來完成類的加載工作的,此時已經表明類加載的啓動已經完成了,即當前執行的類已經編譯,但運行時找不到它的定義時就會拋出NoClassDefFoundError異常,這種情況通常出現在創建一個對象實例的時候(creating a new instance),如在類X中定義了一個類Y,如在類X中定義如下語句ClassY y=new ClassY; 程序運行成功之後(此時X與Y的字節碼文件已經存在),如果將類Y的字節碼文件刪除了重新運行上述代碼,則會在運行時候拋出NoClassDefFoundError異常。當然這只是爲了說明拋出這種異常的原因,一般不會出現刪除該類字節碼情況,實際上是其它原因導致類似刪除的效果導致的,如JAR重複引入,版本不一致導至。因爲jar中都是一些已經編譯好的Class文件,如果存在多個版本那麼在加載的時候就不知道應該調用哪一個版本(相當於刪除字節碼的效果),此種情況一般出現在引入第三方SDK的時候。

總結:

ClassNotFoundException NoClassDefFoundError
從java.lang.Exception繼承,是一個Exception類型 從java.lang.Error繼承,是一個Error類型
當動態加載Class的時候找不到類會拋出該異常 當編譯成功以後執行過程中Class找不到導致拋出該錯誤
一般在執行Class.forName()、ClassLoader.loadClass()或ClassLoader.findSystemClass()的時候拋出 由JVM的運行時系統拋出

ClassNotFoundException發生在裝入階段。 
當應用程序試圖通過類的字符串名稱,使用常規的三種方法裝入類,但卻找不到指定名稱的類定義時就拋出該異常。

NoClassDefFoundError: 當目前執行的類已經編譯,但是找不到它的定義時,發生連接階段:
也就是說你如果編譯了一個類B,在類A中調用,編譯完成以後,你又刪除掉B,運行A的時候那麼就會出現這個錯誤

加載時從外存儲器找不到需要的class就出現ClassNotFoundException 
連接時從內存找不到需要的class就出現NoClassDefFoundError

ClassCastException

在jvm的世界裏,確定是否是相同的一個類需要判斷兩個條件:類的全名 + 類加載器。如果一個相同的類,com.test.Sample,在通過不同的加載器加載後,如果相互賦值,則會出現ClassCastException的異常。

打破雙親委派機制

第一次:在雙親委派模型出現之前—–即JDK1.2發佈之前。 

第二次:模型自身缺點導致:

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱爲“基礎”,是因爲它們總是作爲被用戶代碼調用的API,但世事往往沒有絕對的完美,如果基礎類又要調用回用戶的代碼,那該怎麼辦?
這並非是不可能的事情,一個典型的例子便是JNDI服務,JNDI現在已經是Java的標準服務,它的代碼由啓動類加載器去加載(在JDK 1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啓動類加載器不可能“認識”這些代碼啊!那該怎麼辦?
爲了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoaser()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
有了線程上下文類加載器,就可以做一些“舞弊”的事情了,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

第三次:爲了實現熱插拔,熱部署,模塊化,意思是添加一個功能或減去一個功能不用重啓,只需要把這模塊連同類加載器一起換掉就實現了代碼的熱替換。 

線程上下文加載器的使用場景

  • 當高層提供了統一接口讓低層去實現,同時又要是在高層加載(或實例化)低層的類時,必須通過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類。
  • 當使用本類託管類加載,然而加載本類的ClassLoader未知時,爲了隔離不同的調用者,可以取調用者各自的線程上下文類加載器代爲託管。
     

JIT

JIT是just in time,即時編譯技術。使用該技術,可以加速java程序的運行速度,是一種優化手段。

對於 Java 代碼,剛開始都是被編譯器編譯成字節碼文件,然後字節碼文件會被交由 JVM 解釋執行,所以可以說 Java 本身是一種半編譯半解釋執行的語言。爲了進一步提高代碼的執行速度,HotSpot VM的熱點代碼探測能力可以通過執行計數器找出最具有編譯價值的代碼,然後通知JIT編譯器以方法爲單位進行編譯,提高優化。

編譯器

HotSpot虛擬機內置了兩個即時編譯器:

  • Client Compiler 

        C1編譯器是一個簡單快速的三段式編譯器,主要關注“局部性能優化”,放棄許多耗時較長的全局優化手段 ;

         過程:class -> 1. 高級中間代碼 -> 2. 低級中間代碼 -> 3. 機器代碼

  • Server Compiler 

       C2是專門面向服務器應用的編譯器,是一個充分優化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數時的優化強度。

誰被編譯了?

兩類熱點代碼

  1. 被多次調用的方法 
    • 一個方法被多次調用,理應稱爲熱點代碼,這種編譯也是虛擬機中標準的JIT編譯方式
  2. 被多次執行的循環體 
    • 編譯動作由循環體出發,但編譯對象依然會以整個方法爲對象
    • 這種編譯方式由於編譯發生在方法執行過程中,因此形象的稱爲:棧上替換(On Stack Replacement- OSR編譯,即方法棧幀還在棧上,方法就被替換了)

觸發條件

判斷一段代碼是不是熱點代碼,是不是需要觸發JIT編譯,這樣的行爲稱爲:熱點探測(Hot Spot Detection),有幾種主流的探測方式:

  1. 基於計數器的熱點探測(Counter Based Hot Spot Detection) 
    虛擬機會爲每個方法(或每個代碼塊)建立計數器,統計執行次數,如果超過閥值那麼就是熱點代碼。缺點是維護計數器開銷。

  2. 基於採樣的熱點探測(Sample Based Hot Spot Detection) 
    虛擬機會週期性檢查各個線程的棧頂,如果某個方法經常出現在棧頂,那麼就是熱點代碼。缺點是不精確。

  3. 基於蹤跡的熱點探測(Trace Based Hot Spot Detection) 
    Dalvik中的JIT編譯器使用這種方式

hotspot默認使用計數器的熱點探測:

  • 方法計數器
  • 回邊計數器

目前主流商用JVM都採用編譯器和解釋器並存的架構,但主流商用虛擬機,都同時包含這兩部分。

  1. 當程序需要迅速啓動然後執行的時候,解釋器可以首先發揮作用,編譯器不運行從而省去編譯時間,立即執行程序;

  2. 在程序運行後,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以獲得更高的執行效率。

 

參考資料:

《深入理解jvm虛擬機》

https://blog.csdn.net/yangcheng33/article/details/52631940
https://blog.csdn.net/moakun/article/details/81257897
https://blog.csdn.net/weixin_39222112/article/details/81316511
https://blog.csdn.net/qq_26963433/article/details/78048561
https://www.cnblogs.com/xiaoxian1369/p/5498817.html
https://www.cnblogs.com/insistence/p/5901457.html
http://www.cnblogs.com/aspirant/p/8991830.html
https://blog.csdn.net/chen364567628/article/details/52561588
https://www.cnblogs.com/tinytiny/p/3200448.html
http://www.cnblogs.com/charlesblc/p/5993804.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章