Java類加載器

將類的.class文件讀入內存,併爲之建立一個java.lang.Class的對象,而.class文件只有被確的加載到JVM正中才能運行和使用。java

1、類加載的過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的生命週期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱連接。 程序員

加載

加載階段是「類加載機制」中的一個階段,這個階段一般也被稱做「裝載」,主要完成:數組

  • 經過「類全名」來獲取定義此類的二進制字節流
  • 將字節流所表明的靜態存儲結構轉換爲方法區的運行時數據結構
  • java堆中生成一個表明這個類的java.lang.Class對象,做爲方法區這些數據的訪問入口

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成如下4個階段的校驗動做:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。安全

1.文件格式驗證

這一階段目的是驗證二進制字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理,檢測內容包括如下幾點:數據結構

  • 是否以魔數(0xCAFEBABY)開頭。
  • 主次版本號是否在當前虛擬機處理範圍以內。
  • 常量池中的常量是否有不支持的常量類型。
  • 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
  • CONSTANT_Utf8_info 型常量是否有不符合UTF8數據編碼的數據。
  • Class文件中各個部分及文件自己是否有被刪除的或附加的信息。

2.元數據驗證

這一階段主要對字節碼的描述信息進行語義分析,以保證其描述信息符合java語言規範,這階段的驗證點可能包括如下幾點:jvm

  • 這個類是否有父類
  • 這個類的父類是否繼承了不被容許繼承的類(被final修飾)
  • 若是這個類不是抽象類,是否實現了父類或接口中的方法
  • 類中的字段、方法是否與父類產生矛盾(覆蓋父類的final字段值等)

3.字節碼驗證

這一階段目的主要目的是肯定程序語義是合法的、符合邏輯的。這個階段主要對類的字節碼進行校驗分析,保證該類的方法不會在運行時作出危害虛擬機安全的事:ide

  • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出險操做數棧上 int 類型的數據使用時按long類型加載進本地變量表中。
  • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上。
  • 保證方法體內的類型轉化是有效的,能夠把一個子類對象賦值給父類數據結構,這是安全的,而不能把父類賦值給子類甚至與它無關係的數據類型,這是危險和不合法的。

4.符號引用驗證

這一階段用來將符號引用轉換爲直接引用的時候,這個轉化將在解析階段中發生,符號引用驗證能夠看作是類對自身之外(常量池中各類符號引用)的信息進行匹配性校驗,一般須要校驗如下內容:post

  • 符號引用中可否根據字符串的權限定名找到對應的類。
  • 在指定類中是否存在符合方法的字段描述符以及簡單名稱描述的方法和字段。
  • 符號引用中的類、字段、方法的訪問性是否能夠被當前類訪問。

準備

準備階段是正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存都將在方法區分配。實例變量會在對象實例化的時候跟對象一塊兒在java堆中分配。這裏的初始值指的是一般狀況下的零值。假設一個類變量定義爲:學習

public static int a=123;
複製代碼

那麼變量a初始化的值是0而不是123。若是變量同時是final類型,那麼準備階段就會被賦值爲123,沒必要等到初始化階段再賦值。this

解析

解析階段是將虛擬機常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號進行。可能你們有疑問Class文件中哪有這麼多內容,其實上面也說了,是針對常量池。不論是CLass文件中的方法表仍是字段表,不能直接表示的內容,基本都會直接或間接存在常量池中,所以解析過程就是針對常量池中的數據類型進行解析的。

1.類或接口的解析

要把一個從未解析過的符號引用N解析爲一個類或接口的直接引用,虛擬機須要完成如下3個步驟:

  • 若是C不是一個數組類型,那麼虛擬機會把表明N的權限定名傳遞給D的類加載器去加載這個類C。在加載的過程中,因爲元數據、字節碼驗證的操做,又可能觸發其它類的加載動做,一旦出險任何異常,則解析宣告失敗。
  • 若是C是一個數組類型,而且數組元素爲對象,描述符相似「[Ljava/lang/Integer」的形式按照第一點的規則加載數組元素類型。若是N的描述符如前面所假設的形式,須要加載的類型就是java.lang.Integer,接着由虛擬機生成一個表明次數組維度和元素的數組對象。
  • 若是上面的步驟沒有任何異常,那麼C在虛擬機中實際上已經稱爲一個有效的類或接口了。解析以前還要進行符號驗證,確認D是否具備對C的訪問權限,若是不具有則會拋出異常。

2.字段解析

對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用,若是解析這個類或符號引用的過程當中出現任何異常,都會致使字段符號引用解析的失敗。若是解析成功,這個字段對應的類或接口用C表示,接下來沿着A和它的父類/父接口尋找是否有這個字段,若是有會進行權限驗證,若是不具有權限則拋出異常。若是這個過程不出錯,則會在找到符合字段的時候返回這個字段的直接飲用,查找結束。

3.類(靜態)方法解析類方法解析

  • 首先也要首先解析出類方法表class_index項中索引的方法所屬的類或接口的符號引用,解析成功用C表示。
  • 類方法和接口方法符號引用的常量類型定義是分開的,若是在類方法表中索引類是個接口,直接拋出異常。
  • 若是經過了第一步,在類C中查找是否有簡單名稱和描述符都與目標匹配的方法,有則返回這個方法的直接引用,查找結束。
  • 不然在類的父類遞歸查找是否有這個方法,有則返回直接引用,查找結束。
  • 不然在類的接口列表和父接口遞歸查找,若是存在匹配的方法,說明類C是一個抽象類,查找結束,拋出異常。
  • 不然宣告查找失敗,拋出異常。
  • 最後若是查找成功返回了直接引用,還要對這個方法進行權限驗證,若是不具有權限,則會拋出異常。

4.接口方法解析

接口方法須要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引用。

  • 若是發現class_index中的索引C是個類而不是接口,直接拋出異常。
  • 不然在接口C中查找是否有描述符和名稱都匹配的方法,有則返回方法的直接引用,查找結束。
  • 不然在其父接口中遞歸查找,匹配就返回方法的直接引用,查找結束。
  • 不然宣告方法查找失敗。

初始化

類的初始化階段是類加載過程的最後一步,在準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源,或者能夠從另一個角度來表達:初始化階段是執行類構造器()方法的過程。

在如下四種狀況下初始化過程會被觸發執行:

  • 1.遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則需先觸發其初始化。生成這4條指令的最多見的java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候。
  • 2.使用java.lang.reflect包的方法對類進行反射調用的時候
  • 3.當初始化一個類的時候,若是發現其父類尚未進行過初始化、則須要先出發其父類的初始化
  • 4.jvm啓動時,用戶指定一個執行的主類(包含main方法的那個類),虛擬機會先初始化這個類

初始化類的方法

  • 初始化,也就是new時候會初始化類
  • 訪問類或者接口中的靜態變量或者對其賦值
  • 調用類的靜態方法
  • 反射Class.forName("com.geminno");
  • 初始化它的子類,父類也會初始化
  • 虛擬機啓動時被標明是啓動類的類(java Test),直接用java.exe運行某個類;

類中成員初始化的順序:

父類的靜態字段——>父類靜態代碼塊——>子類靜態字段——>子類靜態代碼塊——>父類成員變量(非靜態字段)——>父類非靜態代碼塊——>父類構造器——>子類成員變量——>子類非靜態代碼塊——>子類構造器
複製代碼

2、類加載器

1.類與類加載器

對於任何一個類,都須要由加載它的類加載器和這個類來確立其在JVM中的惟一性。也就是說,兩個類來源於同一個Class文件,而且被同一個類加載器加載,這兩個類才相等。

2.雙親委派模型

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

大部分Java程序通常會使用到如下三種系統提供的類加載器:

啓動類加載器(Bootstrap ClassLoader):負責加載JAVA_HOME\lib目錄中而且能被虛擬機識別的類庫到JVM內存中,若是名稱不符合的類庫即便放在lib目錄中也不會被加載。該類加載器沒法被Java程序直接引用。
擴展類加載器(Extension ClassLoader):該加載器主要是負責加載JAVA_HOME\lib\,該加載器能夠被開發者直接使用。
應用程序類加載器(Application ClassLoader):該類加載器也稱爲系統類加載器,它負責加載用戶類路徑(Classpath)上所指定的類庫,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

雙親委派模型的源碼實現:

主要體如今ClassLoaderloadClass()方法中,思路很簡單:先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父類加載器爲空則默認使用啓動類加載器做爲父類加載器。若是父類加載器加載失敗,拋出ClassNotFoundException異常後,調用本身的findClass()方法進行加載。

三.自定義類加載器

若要實現自定義類加載器,只須要繼承java.lang.ClassLoader類,而且重寫其findClass()方法便可java.lang.ClassLoader 類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,而後從這些字節代碼中定義出一個Java 類,即java.lang.Class類的一個實例。除此以外,ClassLoader還負責加載Java 應用所需的資源,如圖像文件和配置文件等,ClassLoader中與加載類相關的方法以下:

方法說明

getParent()  返回該類加載器的父類加載器。

loadClass(String name) 加載名稱爲 二進制名稱爲name 的類,返回的結果是 java.lang.Class 類的實例。

findClass(String name) 查找名稱爲 name 的類,返回的結果是 java.lang.Class 類的實例。

findLoadedClass(String name) 查找名稱爲 name 的已經被加載過的類,返回的結果是 java.lang.Class 類的實例。

resolveClass(Class<?> c) 連接指定的 Java 類。
複製代碼
/** * 1、ClassLoader加載類的順序 * 1.調用 findLoadedClass(String) 來檢查是否已經加載類。 * 2.在父類加載器上調用 loadClass 方法。若是父類加載器爲 null,則使用虛擬機的內置類加載器。 * 3.調用 findClass(String) 方法查找類。 * 2、實現本身的類加載器 * 1.獲取類的class文件的字節數組 * 2.將字節數組轉換爲Class類的實例 * @author lei 2011-9-1 */
	public class ClassLoaderTest {
	    public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
	        //新建一個類加載器
	        MyClassLoader cl = new MyClassLoader("myClassLoader");
	        //加載類,獲得Class對象
	        Class<?> clazz = cl.loadClass("classloader.Animal");
	        //獲得類的實例
	        Animal animal=(Animal) clazz.newInstance();
	        animal.say();
	    }
	}
複製代碼
class Animal{
	    public void say(){
	        System.out.println("hello world!");
	    }
	}
複製代碼
class MyClassLoader extends ClassLoader {
	    //類加載器的名稱
	    private String name;
	    //類存放的路徑
	    private String path = "E:\\workspace\\Algorithm\\src";
	    MyClassLoader(String name) {
	        this.name = name;
	    }
	    MyClassLoader(ClassLoader parent, String name) {
	        super(parent);
	        this.name = name;
	    }
	    /** * 重寫findClass方法 */
	    @Override
	    public Class<?> findClass(String name) {
	        byte[] data = loadClassData(name);
	        return this.defineClass(name, data, 0, data.length);
	    }
	    public byte[] loadClassData(String name) {
	        try {
	            name = name.replace(".", "//");
	            FileInputStream is = new FileInputStream(new File(path + name + ".class"));
	            ByteArrayOutputStream baos = new ByteArrayOutputStream();
	            int b = 0;
	            while ((b = is.read()) != -1) {
	                baos.write(b);
	            }
	            return baos.toByteArray();
	        } catch (Exception e) {
	            e.printStackTrace();
	        }
	        return null;
	    }
	}

複製代碼

四.問題

1.根據類加載器加載類的初始化原理,推斷如下代碼的輸入結果爲?

public class Test {
    public static void main(String[] args) throws Exception{
      ClassLoader classLoader=ClassLoader.getSystemClassLoader();
      Class clazz=classLoader.loadClass("A");
      System.out.print("Test");
      clazz.forName("A");
    }
}

class A{
    static {
        System.out.print("A");
    }
}
複製代碼

解答:

2.繼承是JAVA語言的一個特性,針對類的繼承,虛擬機會如何進行父類和子類的初始化加載呢?請閱讀代碼選擇出該段代碼的輸入結果?

public class Test {
    public static void main(String[] args) {
        System.out.print(B.c);
    }
}

class A {
    public static String c = "C";
    static {
        System.out.print("A");
    }
}

class B extends A{
    static {
        System.out.print("B");
    }
}
複製代碼

解答:

3.JAVA的類加載期負責整個生命週期內的class的初始化和加載工做,就虛擬機的規範來講,如下代碼會輸出什麼結果?

public class Test {
    public static void main(String[] args) {
        System.out.println(Test2.a);
    }
}

class Test2{
    public static final String a=new String("JD");
    static {
        System.out.print("OK");
    }
}
複製代碼

解答:

4.能不能本身寫個類叫java.lang.System

答案:一般不能夠,但能夠採起另類方法達到這個需求。
解釋:爲了避免讓咱們寫System類,類加載採用委託機制,這樣能夠保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加載。而System是Bootstrap加載器加載的,就算本身重寫,也老是使用Java系統提供的System,本身寫的System類根本沒有機會獲得加載。 可是,咱們能夠本身定義一個類加載器來達到這個目的,爲了不雙親委託機制,這個類加載器也必須是特殊的。因爲系統自帶的三個類加載器都加載特定目錄下的類,若是咱們本身的類加載器放在一個特殊的目錄,那麼系統的加載器就沒法加載,也就是最終仍是由咱們本身的加載器加載。

引用參考:
juejin.im/post/5a1d64…
blog.csdn.net/boyupeng/ar…
blog.csdn.net/boyupeng/ar…

備註:文章內容還在深刻學習,會不斷修改跟新!

相關文章
相關標籤/搜索