1. 程式人生 > >JVM活學活用——類加載機制

JVM活學活用——類加載機制

java模塊化 throw framework 本地文件 dcl 也會 javac 限定 java

類的實例化過程


有父類的情況

1. 加載父類靜態
1.1 為靜態屬性分配存儲空間並賦初始值
1.2 執行靜態初始化塊和靜態初始化語句(從上至下)

2. 加載子類靜態
2.1 為靜態屬性分配存儲空間
2.2 執行靜態初始化塊和靜態初始化語句(從上至下)

3. 加載父類非靜態
3.1 為非靜態塊分配空間  
3.2 執行非靜態塊

4. 加載子類非靜態
4.1 為非靜態塊分配空間  
4.2 執行非靜態塊

5. 加載父類構造器
5.1 為實例屬性分配存數空間並賦初始值
5.2 執行實例初始化塊和實例初始化語句
5.3 執行構造器內容

6. 加載子類構造器
6.1 為實例屬性分配存數空間並賦初始值
6.2 執行實例初始化塊和實例初始化語句
6.3 執行構造器內容

下面看一個例子:

package jvm;

public class InstanceClass extends ParentClass{

    public static String subStaticField = "子類靜態變量";
    public String subField = "子類非靜態變量";
    public static StaticClass staticClass = new StaticClass("子類");

    static {
        System.out.println("子類 靜態塊初始化");
    }

    {
        System.out.println(
"子類 [非]靜態塊初始化"); } public InstanceClass(){ System.out.println("子類構造器初始化"); } public static void main(String args[]) throws InterruptedException { new InstanceClass(); } } class ParentClass{ public static String parentStaticField = "父類靜態變量"; public String parentField = "父類[非]靜態變量";
public static StaticClass staticClass = new StaticClass("父類"); static { System.out.println("父類 靜態塊初始化"); } { System.out.println("父類 [非]靜態塊初始化"); } public ParentClass(){ System.out.println("父類 構造器初始化"); } } class StaticClass{ public StaticClass(String name){ System.out.println(name+" 靜態變量加載"); } }

按照上面說的規則,先自己想一想,然後再查看答案:

父類 靜態變量加載
父類 靜態塊初始化
子類 靜態變量加載
子類 靜態塊初始化
父類 [非]靜態塊初始化
父類  構造器初始化
子類 [非]靜態塊初始化
子類構造器初始化

拋磚引玉之後,結合《深入理解Java虛擬機》看看類加載機制

什麽是Java類的加載?


  類的加載是指將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後再堆區創建一個java.lang.class對象,用來封裝類的方法區內的數據結構。類的加載的最終產品是位於堆區的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。在Java語言中,類型的加載、連接、初始化過程都是在程序運行區間完成的。

                技術分享圖片

  類加載器並不需要等到某各類被首次主動使用時再加載它,JVM規範允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麽類加載器就不會報告錯誤。

  加載.class文件的方式:

  1.   – 從本地系統中直接加載
  2.    – 通過網絡下載.class文件
  3. – 從zip,jar等歸檔文件中加載.class文件
  4. – 從專有數據庫中提取.class文件
  5. – 將Java源文件動態編譯為.class文件

類的生命周期


技術分享圖片

  類從被加載到虛擬機內存開始,到卸載出內存為止,它的整個生個生命周期包括:加載,驗證,準備,解析,初始化,使用,卸載共7個階段。其中驗證,準備,解析統稱為連接。

  其中類的加載過程包括(加載、驗證、準備、解析、初始化)五個階段。這五個階段中,加載,驗證,準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化之後開始,這是為了支持Java的運行時綁定(也成為動態綁定或者晚期綁定).

  另外註意這裏的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。

加載——查找並加載類的二進制數據

  加載時類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:

  1.通過一個類的全限定名來的獲取定義此類的二進制字節流

  2.將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構

  3.在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據訪問入口。

  相對於類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。

  對於數組類而言,情況就有所不同,數組類本身不通過類加載器創建,它是由虛擬機直接創建的。但數組類和類加載器仍然有很密切的關系,因為數組類的元素類型最終要靠類加載器去創建。

  加載過程完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機自行定義,虛擬機規範未規定此區域的具體數據結構。然後在內存中實例化一個java.lang.Class類的對象,這樣對象將作為程序訪問方法區中的這些類型數據的外部接口。

驗證——確保被加載的類的正確性

  驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會虛擬機自身的安全。驗證階段大致會完成4個階段

的檢驗工作:

  文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。 

  元數據驗證:對字節碼描述的信息進行語義分析(註意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。

  字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

  符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那麽可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

準備——為類的靜態變量分配內存,並將其初始化為默認值

  準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要註意:

  1.這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時伴隨著對象一塊分配到Java堆中

  2.這裏所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

  假設一個類變量的定義為:public static int value = 3;

那麽變量value在準備階段過後的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯後,存放於類構造器<clinit>()方法之中的,所以把value賦值為3的動作將在初始化階段才會執行。

  這裏還需要註意如下幾點:

  • 對基本數據類型來說,對於類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值,而對於局部變量來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
  • 對於同時被static和final修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;
  • 而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予默認零值。
  • 對於引用數據類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予默認的零值,即null。 · 如果在數組初始化時沒有對數組中的各元素賦值,那麽其中的元素將根據對應的數據類型而被賦予默認的零值。

 3.如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麽在準備階段變量value就會被初始化為ConstValue屬性所指定的值。

  例:public static final int value = 3;

  編譯時Javac將會value生成ConstantValue屬相,在準備階段虛擬機就會根據ConstantValue的設置將value賦值為3.

  可以理解static final常量在編譯期就將其結果放入了調用它的類的常量池中。

解析——把類中的符號引用轉換為直接引用

  解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。

  直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

初始化

  初始化,為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。Java中對類變量進行初始化設定有兩種方式:

  1.聲明類變量式指定初始值

  2.使用靜態代碼塊為類變量指定初始值

  重點:JVM初始化步驟

  1.假如這個類還沒有被加載和連接,則程序先加載並連接該類

  2.假設該類的直接父類還沒有被初始化,則先初始化其直接父類

  3.假如類中有初始化語句,則系統依此執行這些初始化語句

  

  類的初始化時機:只有當類主動使用的時候才會導致類的初始化。類的主動使用包括以下六種:

  1.   創建類的實例,也就是new的方式
  2.   訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  3.   調用類的靜態方法
  4.   反射(如Class.forName(“com.Test”))
  5.   初始化某個類的子類,則其父類也會被初始化
  6.   Java虛擬機啟動時被表明為啟動類的類(Java Test),直接使用java.ext命令來運行主類。

  

結束生命周期

  在如下幾種情況下,Java虛擬機將結束生命周期

  – 執行了System.exit()方法

  – 程序正常執行結束

  – 程序在執行過程中遇到了異常或錯誤而異常終止

  – 由於操作系統出現錯誤而導致Java虛擬機進程終止

類加載器


尋找類加載器的例子:

public class TestClassLoadDemo {
    public static void main(String[] args){
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

輸出結果:

技術分享圖片

  從上面的結果可以看出,並沒有獲取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引導類加載器)是用C語言實現的,找不到一個確定的返回父Loader的方式,於是就返回null。

  這幾種類加載器的層次關系如下圖所示:

  技術分享圖片

註意:這裏父類加載器並不是通過繼承關系來實現的,而是采用組合實現的。

站在Java虛擬機的角度來講,只存在兩種不同的類加載器:

啟動類加載器:它使用C++實現(這裏僅限於Hotspot,也就是JDK1.5之後默認的虛擬機,有很多其他的虛擬機是用Java語言實現的),是虛擬機自身的一部分;

所有其他的類加載器:這些類加載器都由Java語言實現,獨立於虛擬機之外,並且全部繼承自抽象類java.lang.ClassLoader,這些類加載器需要由啟動類加載器加載到內存中之後才能去加載其他的類。

站在Java開發人員的角度來看,類加載器可以大致劃分為以下三類:

啟動類加載器:Bootstrap ClassLoader,負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啟動類加載器是無法被Java程序直接引用的。

擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器。

應用程序類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因為JVM自帶的ClassLoader只是懂得從本地文件系統加載標準的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:

1)在執行非置信代碼之前,自動驗證數字簽名。

2)動態地創建符合用戶特定需要的定制化構建類。

3)從特定的場所取得java class,例如數據庫中和網絡中。

JVM類加載機制

全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴的引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。

父類委托:先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。

緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類的二進制數據,並將其轉換成Class對象,存入緩存區。

這就是為什麽修改了Class後,必須重啟JVM,程序的修改才會生效。

類的加載


類的加載有三種方式:

1.命令行啟動應用時候由JVM初始化加載

2.通過Class.forName方法動態加載

3.通過ClassLoader.loadClass()方法動態加載

例子:

 1 public class LoadTest {
 2     public static void main(String args[]) throws ClassNotFoundException {
 3         ClassLoader loader = Thread.currentThread().getContextClassLoader();
 4         System.out.println(loader);
 5 
 6         // 使用ClassLoader.loadClass()來加載類,不會執行初始化塊
 7         loader.loadClass("jvm.TestClassLoad");
 8 
 9         // 使用Class.forName()來加載類,默認會執行初始化塊
10         Class.forName("jvm.TestClassLoad");
11 
12         //使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊
13         Class.forName("jvm.TestClassLoad", false, loader);
14     }
15 }

要加載的類:

package jvm;

public class TestClassLoad {
    static {
        System.out.println("靜態代碼塊被加載了");
    }
}

輸出結果:

技術分享圖片

Class.forName()和ClassLoader.loadClass()區別

Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;

ClassLoader.loadClass():只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。

Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法采用調用構造函數,創建類的對象 。

雙親委派模型


  雙親委派的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載類。

雙親委派機制

1.當AppClassLoader加載一個class時,他首先不會自己去嘗試加載這個類,而是把類加載請求委托給父類加載器ExtClassLoader去完成。

2.當ExtClassLoader加載一個class時,他首先也不會自己去嘗試加載這個類,而是把類加載請求委托給BootStrapClassLoader去完成。

3.如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;

4、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。

ClassLoader源碼分析:

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 Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

雙親委派模型意義

-系統類防止內存中出現多份同樣的字節碼

-保證Java程序安全穩定運行

破壞雙親委派模型

  1. 第一次破壞是在jdk2之前,用戶自定義的類加載器都是重寫Classloader中的loadClass方法,這樣就導致每個自定義的類加載器其實是在使用自己的loadClass方法中的加載機制來進行加載,這種模式當然是不符合雙親委派機制的,也是無法保證同一個類在jvm中的唯一性的,那麽為了保證及時是由不同的類加載器(哪怕是用戶自定義的類加載器加載)也是唯一的,java官方在Classloader中添加了findClass方法,用戶只需要重新這個findClass方法,在loadClass方法的邏輯裏,如果父類加載失敗的時候,才會調用自己的findClass方法來完成類加載,這樣就完成了符合雙親委派機制。

  2. 第二次的破壞是類似於jndi,jdbc這種服務,因為這種服務需要回調用戶的代碼,但是對於父類加載器而言是不認識用戶的代碼的。

    那麽這時候java團隊使用了一個不太優雅的設計:線程上下文類加載器。這個類加載器可以通過Thread類的setContextClassLoader方法進行設置,如果創建線程時還未設置,它就從父線程繼承一個,如果在應用全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

利用這個線程上下文類加載器,jdni去加載需要的spi代碼,也就是父類請求子類的加載器去加載。

  1. 第三次的破壞是因為用戶對於程序的動態性追求,諸如:代碼熱替換,模塊熱部署。
    這時候就誕生了諸如jigsaw和osgi。對於現在的業界來講,osgi贏得了java模塊化的主導權,成為目前業界模塊化的標準。而Osgi模塊話的關鍵是他自己的類加載機制:每個程序模塊(bundle)都有自己的類加載器,需要更換程序(bundle)的時候,連同類加載器一起替換,以實現代碼的熱部署

osgi和雙親委派模式不同,他是一個基於網狀的互相組合依賴的加載。
Osgi的加載步驟是這樣的:

  1. 如果類或者資源是在包java.*中,那麽交由父級類加載器代理完成,否則,搜索過程進入第二步。如果父類級類加載器加載失敗,那麽查找過程結束,加載失敗。
  2. 如果類或者資源在啟動代理序列(org.osgi.framework.bootdelegation)中定義,那麽交由父級代理完成,此時的父級代理有啟動參數org.osgi.framework.bundle.parent指定,默認是引導類加載器(bootstrap class loader),如果找到了類或者資源,那麽查找過程結束。
  3. 如果類或者資源所在的包是在Import-Package中指定的,或者是在此之前通過動態導入加載的了,那麽將請求轉發到導出bundle的類加載器,否則搜索繼續進行下一步;如果該包在啟動參數org.osgi.framework.system.packages.extra中,則將請求轉發給osgi容器外部的類加載器(通常是系統類加載器)。如果將請求交由導出類加載器代理,而類或者資源又沒有找到,那麽查找過程中止,同時請求失敗。
  4. 如果包中類或者和資源所在的包由其他bundle通過是使用Require-Bundle從一個或多個其他bundle進行導入的了,那麽請求交由其他那些bundle的類加載器完成,按照根據在bundle的manifest中指定的順序進行查找進行查找。如果沒有找到類或者資源,搜索繼續進行。
  5. 使用bundle本身的內部bundle類路徑查找完畢之後,。如果類或者資源還沒有找到,搜索繼續到下一步。
  6. 查找每一個附加的fragment的內部類路徑,fragment的查找根據bundle ID順序升序查找。如果沒有找到類或者資源的,查找過程繼續下一步。
  7. 如果包中類或者資源所在的包由bundle導出,或者包由bundle導入(使用Import-Package或者Require-Bundle),查找結束,即類或者資源沒有找到。
  8. 否則,如果類或者資源所在的包是通過使用DynamicImport-Package進行導入,那麽試圖進行包的動態導入。導出者exporter必須符合包約束。如果找到了合適的導出者exporter,然後建立連接,以後的包導入就可以通過步驟三進行。如果連接建立失敗,那麽請求失敗。
  9. 如果動態導入建立了,請求交由導出bundle的類加載器代理。如果代理查找失敗,那麽查找過程中止,請求失敗

自定義類加載器

通常情況下,我們都是直接使用系統類加載器。但是,有的時候,我們也需要自定義類加載器。比如應用是通過網絡來傳輸 Java 類的字節碼,為保證安全性,這些字節碼經過了加密處理,這時系統類加載器就無法對其進行加載,這樣則需要自定義類加載器來實現。自定義類加載器一般都是繼承自 ClassLoader 類,從上面對 loadClass 方法來分析來看,我們只需要重寫 findClass 方法即可。下面我們通過一個示例來演示自定義類加載器的流程:

package com.classloader;

import java.io.*;


public class MyClassLoader extends ClassLoader {

    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace(‘.‘, File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定義類加載器的核心在於對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。由於這裏只是演示,我並未對class文件進行加密,因此沒有解密的過程。這裏有幾點需要註意:

1、這裏傳遞的文件名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。

2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委托模式。

3、這類Test 類本身可以被 AppClassLoader 類加載,因此我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則,由於雙親委托機制的存在,會直接導致該類由 AppClassLoader 加載,而不會通過我們自定義類加載器來加載。

參考:

http://blog.csdn.net/ns_code/article/details/17881581

https://www.cnblogs.com/ityouknow/p/5603287.html

JVM活學活用——類加載機制