Java類裝載過程與類裝載器

1. class裝載驗證流程

1.1 加載

  • 裝載類的第一個階段
  • 經過類的全限定名取得類的二進制流
  • 轉爲方法區數據結構
  • 在Java堆中生成對應的java.lang.Class對象

1.2 連接 -> 驗證

目的:保證Class流的格式是正確的html

文件格式的驗證java

  • 是否以0xCAFEBABE開頭
  • 版本號是否合理

元數據驗證設計模式

  • 是否有父類
  • 繼承了final類?
  • 非抽象類實現了全部的抽象方法

字節碼驗證 (很複雜)數組

  • 運行檢查
  • 棧數據類型和操做碼數據參數吻合
  • 跳轉指令指定到合理的位置

符號引用驗證安全

  • 常量池中描述類是否存在
  • 訪問的方法或字段是否存在且有足夠的權限

1.3 連接 -> 準備

分配內存,併爲類設置初始值 (方法區中,關於方法區請查看Java內存區域網絡

  • public static int v=1;
  • 在準備階段中,v會被設置爲0
  • 在初始化的<clinit>中才會被設置爲1
  • 對於static final類型(常量),在準備階段就會被賦上正確的值
  • public static final  int v=1;

1.4 連接 -> 解析

解析是惟一一個不肯定順序的過程,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持 Java 語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。數據結構

這裏簡要說明下 Java 中的綁定:綁定指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,對 Java 來講,綁定分爲靜態綁定和動態綁定:app

  • 靜態綁定:即前期綁定。在程序執行前方法已經被綁定,此時由編譯器或其它鏈接程序實現。針對 Java,簡單的能夠理解爲程序編譯期的綁定。Java 當中的方法只有 final,static,private 和構造方法是前期綁定的。
  • 動態綁定:即晚期綁定,也叫運行時綁定。在運行時根據具體對象的類型進行綁定。在 Java 中,幾乎全部的方法都是後期綁定的。

符號引用替換爲直接引用框架

符號引用只是一種表示的方式,好比某個類繼承java.lang.object,在符號引用階段,只會記錄該類是繼承"java.lang.object",以這種字符串的形式保存,可是不能保證該對象被記載。ide

直接引用就是真正能使用的引用,它是指針或者地址偏移量,引用對象必定在內存。最終知道在內存中到底放在哪裏。

替換後,Class才能索引到它要用的那些內容。

一、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,仍是普通的對象類型的引用,從而進行不一樣的解析。

二、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,若是有,則查找結束;若是沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,尚未,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束,查找流程以下圖所示:

從下面一段代碼的執行結果中很容易看出來字段解析的搜索順序:

class Super{  
    public static int m = 11;  
    static{  
        System.out.println("執行了super類靜態語句塊");  
    }  
}  

class Father extends Super{  
    public static int m = 33;  
    static{  
        System.out.println("執行了父類靜態語句塊");  
    }  
}  

class Child extends Father{  
    static{  
        System.out.println("執行了子類靜態語句塊");  
    }  
}  

public class StaticTest{  
    public static void main(String[] args){  
        System.out.println(Child.m);  
    }  
}

執行結果以下:

執行了super類靜態語句塊
執行了父類靜態語句塊
33

若是註釋掉 Father 類中對 m 定義的那一行,則輸出結果以下:

執行了super類靜態語句塊
11

static變量發生在靜態解析階段,也便是初始化以前,此時已經將字段的符號引用轉化爲了內存引用,也便將它與對應的類關聯在了一塊兒,因爲在子類中沒有查找到與 m 相匹配的字段,那麼 m 便不會與子類關聯在一塊兒,所以並不會觸發子類的初始化。

至於爲何先執行super的類靜態語句塊,後執行父類靜態語句塊是由於在初始化階段子類的<clinit>調用前保證父類的<clinit>被調用

最後須要注意:理論上是按照上述順序進行搜索解析,但在實際應用中,虛擬機的編譯器實現可能要比上述規範要求的更嚴格一些。若是有一個同名字段同時出如今該類的接口和父類中,或同時在本身或父類的接口中出現,編譯器可能會拒絕編譯。若是對上面的代碼作些修改,將 Super 改成接口,並將 Child 類繼承 Father 類且實現 Super 接口,那麼在編譯時會報出以下錯誤:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The field Child.m is ambiguous

	at StaticTest.main(StaticTest.java:20)

三、類方法解析:對類方法的解析與對字段解析的搜索步驟差很少,只是多了判斷該方法所處的是類仍是接口的步驟,並且對類方法的匹配搜索,是先搜索父類,再搜索接口

四、接口方法解析:與類方法解析步驟相似,只是接口不會有父類,所以,只遞歸向上搜索父接口就好了。

1.5 初始化

執行類構造器<clinit>

  • static變量 賦值語句
  • static{}語句

子類的<clinit>調用保證父類的<clinit>被調用

所以,在虛擬機中第一個被執行的()方法的類確定是java.lang.Object

接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操做,所以接口與類同樣會生成()方法。可是接口與類不一樣的是:執行接口的()方法不須要先執行父接口的()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的()方法。

實驗:

package test;

public class Test extends A
{
	static {
		System.out.println("Test");
	}
	public static void main(String[] args)
	{
		A a = new A();
	}
}

class A {
	static {
		System.out.println("A");
	}
}

輸出:

A
Test

<clinit>是線程安全的

那麼Java.lang.NoSuchFieldError錯誤可能在什麼階段拋出呢?

很顯然是在連接的驗證階段的符號引用驗證時會拋出這個異常,或者NoSuchMethodError等異常。

2. 什麼是類裝載器ClassLoader

  • ClassLoader是一個抽象類
  • ClassLoader的實例將讀入Java字節碼將類裝載到JVM中
  • ClassLoader能夠定製,知足不一樣的字節碼流獲取方式(譬如從網絡中加載,從文件中加載)
  • ClassLoader負責類裝載過程當中的加載階段

2.1 ClassLoader的重要方法

public Class<?> loadClass(String name) throws ClassNotFoundException

載入並返回一個Class

protected final Class<?> defineClass(byte[] b, int off, int len)

定義一個類,不公開調用

protected Class<?> findClass(String name) throws ClassNotFoundException

loadClass回調該方法,自定義ClassLoader的推薦作法

protected final Class<?> findLoadedClass(String name)

尋找已經加載的類

2.2 系統中的ClassLoader

  • BootStrap ClassLoader (啓動ClassLoader)
  • Extension ClassLoader (擴展ClassLoader)
  • App ClassLoader (應用ClassLoader/系統ClassLoader)
  • Custom ClassLoader(自定義ClassLoader)

每一個ClassLoader都有一個Parent做爲父親( BootStrap除外)

3. JDK中ClassLoader默認設計模式

自底向上檢查類是否被加載,通常狀況下,首先先從AppClassLoader中調用findLoadedClass查看是否已經加載,若是沒有加載,則會交給父類,ExtensionClassLoader去查看是否加載,還沒加載,則再調用其父類,BootstrapClassLoader查看是否已經加載,若是仍然沒有,自頂向下嘗試加載類,那麼從 BootstrapClassLoader到 AppClassLoader依次嘗試加載。

即便兩個類來源於同一個 Class 文件,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。這裏的「相等」包括了表明類的 Class 對象的 equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用 instanceof 關鍵字對對象所屬關係的斷定結果。

從代碼上能夠很容易看出來,首先先本身查看這個類是否被調用,若是沒有找到,則調用父親的loadClass,直到BootstrapClassLoader(沒有父親)。

咱們把這個加載的過程叫作雙親模式。

雙親委託機制的做用是防止系統jar包被本地替換

3.1 雙親模式的問題

這種模式下會有一個問題:

頂層ClassLoader,沒法加載底層ClassLoader的類

Java框架(rt.jar)如何加載應用的類?

好比:javax.xml.parsers包中定義了xml解析的類接口
Service Provider Interface SPI 位於rt.jar 
即接口在啓動ClassLoader中。
而SPI的實現類,在AppLoader。

這樣就沒法用BootstrapClassLoader去加載SPI的實現類。

3.2 解決

JDK中提供了一個方法:

Thread. setContextClassLoader()
  • 上下文加載器
  • 是一個角色
  • 用以解決頂層ClassLoader沒法訪問底層ClassLoader的類的問題
  • 基本思想是,在頂層ClassLoader中,傳入底層ClassLoader的實例

上下文ClassLoader能夠突破雙親模式的侷限性

3.3 雙親模式的破壞

  • 雙親模式是默認的模式,但不是必須這麼作
  • Tomcat的WebappClassLoader 就會先加載本身的Class,找不到再委託parent
  • OSGi的ClassLoader造成網狀結構,根據須要自由加載Class

Reference:

1. http://www.cnblogs.com/snake-hand/p/3151381.html

2. http://www.cnblogs.com/Lawson/archive/2012/07/31/2616623.html

3. http://wiki.jikexueyuan.com/project/java-vm/class-loading-mechanism.html

相關文章
相關標籤/搜索