java類的加載過程

在<深刻理解Java虛擬機-周志明>這本書裏面,在講到類初始化的五種狀況時,說起了一個比較有趣的事情。先來看看下面的代碼java

public class SubClass {
    static{
        System.err.println("I m your son");
    }
    public static final int name = 111;
}

這個時候若是調用SubClass.name,是根本不會觸發SubClass初始化的(這裏是由於name是一個常量,和下面的例子不同,若是這裏把final去掉,是會觸發Subclass的初始化的,由於對於靜態字段而言,若是靜態字段被引用,就會調用getstatic指令和putstatic指令,那麼天然就會引起類的初始化,詳情看下面關於觸發類初始化的五種狀況)。再來看看另外一種狀況;數組

public class SuperClass {

    static{
        System.err.println("I am your father");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass{
    static{
        System.err.println("I m your son");
    }
}

這個時候若是調用SubClass.value(靜態字段和靜態方法是能夠繼承可是沒法被覆蓋,因此這裏調用value,只會致使直接定義這個靜態變量的類被初始化),一樣也是不會使得SubClass這個類進行初始化。那麼問題來了,到底類在何時會進行初始化,類的初始化順序究竟是怎樣的?讓咱們接着往下看。緩存

一. 類加載的過程
虛擬機加載類主要有五個過程:加載、驗證、準備、解析和初始化。安全

  1. 加載:加載是「類加載」的一個過程,但願讀者沒有混淆這兩個概念。

在這個過程虛擬機主要完成三件事,
 經過一個類的全限定名___[解釋全限定名]___來獲取此類的二進制字節流,這點上,虛擬機並無指明要從哪裏獲取類的二進制字節流,所以發展出了不少不同的加載方式。好比jar,zip等壓縮包中加載,從網絡獲取[如Applet],或者由其餘文件生成[如從JSP生成]。
 將字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
 在Java堆[這個沒有強制規定,好比HotSpot則選擇在方法區中生成這個對象]中生成一個表明這個類的java.lang.Class對象,做爲程序訪問方法區中的各類數據的外部入口[也就是說當常量池表中的數據被轉換成運行時數據結構的時候,實際上[堆/方法區]有一個Class對象的實例能夠訪問到方法區的各種數據,包括常量池表,代碼等]。
若是加載對象是普通的類或者接口(統稱爲C),則是經過類加載器(L)去加載C的二進制表示來建立。可是若是加載的是數組類,那狀況就有所不一樣了,數組類自己不經過類加載器建立,它是由Java虛擬機直接建立的。可是數組類內部的元素類型最終仍是要靠類加載器去加載。[後續能夠添加類加載器的詳細解釋]網絡

  1. 驗證

驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自己的安全。驗證大體上有如下4個過程:
1) 文件格式驗證:
a) 檢查魔數,主、次版本號是否在當前虛擬機處理範圍。
b) 常量池的常量是否不被支持[經過檢查tag],指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
c) CONSTANT_Utf8_info類型的常量中是否有不符合UTF8編碼的數據。
d) Class文件中各個部分及文件自己是否有被刪除或者附加其餘信息等等。
這個節點的主要目的是保證輸入的字節流能被正確的解析並存儲於方法區內,格式上符合描述一個java類型信息的要求。這個階段是基於二進制流,只要經過了這個階段的驗證,字節流纔會進入內存的方法區中存儲。因此後續的三個階段基於方法區的存儲結構進行的,不會再直接操做字節流。
2) 元數據驗證:這個階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。主要驗證包括如下幾點:
a) 這個類是否有父類(除了java.lang.Object外,全部的類都應該有父類)。
b) 這個類的父類是否繼承了不容許被繼承的類(被final修飾的類)。
c) 若是這個類不是抽象類,那麼應該實現其父類或接口中要求實現的方法。
d) 類中的字段,方法是否與父類相矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載)。
這個階段主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java規範的元數據信息。
3) 字節碼驗證:
4) 符號引用驗證:數據結構

  1. 準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,這些變量所使用的內存都將在方法區中分配。這裏有幾個值得注意的點:
1) 這裏初始化的僅僅是類變量(被static修飾的變量)的初始化,並不包括實例變量。實例變量將會在對象實例化的時候隨着對象一塊兒分配在java堆中。
2) 這裏所說的初始值,一般是數據類型的零值,舉個例子:
public static int value = 123;
這句代碼中,value在準備階段的初始值爲0,而不是123,由於這個時候還沒開始執行任何的java方法。而把value的值置爲123的putstatic指令是程序被編譯後,存放在類構造器<clinit>()方法中的。因此value置爲123是在初始化[第五階段]階段纔會執行。[還有一些其餘類型的零值,能夠參考虛擬機規範]
固然,上述狀況也有例外的地方,若是類字段的字段屬性表(參考class文件中的屬性數據結構)中存在ConstatntValue[即同時被final和static修飾]屬性,那麼在準備階段,變量value就會被初始化爲ConstantValue屬性所指定的值,例如上述變量中,編譯時javac將會爲value生成的ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue屬性而將value賦值爲123。佈局

  1. 解析

解析階段就是虛擬機將常量池內的符號引用[使用一組描述符來描述所引用的目標,符能夠是任意形式的字面量,只要使用時能無歧義的定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中]替換爲直接引用[直接引用能夠是直接指向目標的指針,相對偏移量或者一個能間接定位到目標的句柄。直接引用與內存的佈局有關,若是有了直接引用,則目標必定存在]的過程,符號引用在Class文件內的常量池中以CONSTANT_Fieldref_info,CONSTANT_Class_info,CONSTANT_Methodref_info等類型出現。那麼,解析階段中的直接引用於符號引用又有什麼關聯呢?
對同一個符號引用進行屢次解析請求是很常見的,好比你在代碼裏面屢次new同一個類。這裏要分紅兩種狀況:
1) invokeddynamic指令:這個指令的特殊之處在於,它是爲了支持動態語言而存在的,也就是說,必須等到程序實際運行這條指令的時候,解析動做才能進行[目前僅使用java語言並不會生成這條指令]。相對的,其他觸發的解析指定都是「靜態」的,能夠在剛剛完成加載階段,還沒開始執行代碼時就進行解析。
2) 除了上述的指令外,虛擬機實現能夠對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標識爲已解析狀態)。從而避免了屢次解析。
解析動做主要針對「類或接口」,「字段」,「類方法」,「接口方法」,「方法類型」,「方法句柄」和「調用點限定符」7類符號引用進行[分別對應7種常量池表的CONSTATN_Class_info,CONSTATN_Fieldref_info,CONSTATN_Methodref_info,CONSTATN_InterfaceMethodref_info,CONSTATN_MethodType_info,CONSTATN_MethodHandle_info,CONSTATN_InvokeDynamic_info,後續三種和動態類型有關,目前java仍是靜態類型語言]。編碼

  1. 初始化

在虛擬機中嚴格規定須要對類進行初始化的,有下面五種狀況:
1) 遇到new,getstatic,putstatic或者invokestatic這4條字節碼指令時。
2) 使用java.lang.reflect包的方法對類進行反射調用的時候。
3) 當初始化一個類,發現其父類並無初始化時,須要先初始化父類。
4) 虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
5) 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有初始化,則須要先觸發其初始化。
對於以上五種初始化場景,虛擬機規範中使用了「只有」,除此以外,全部的引用類的方式都不會觸發初始化。指針

相關文章
相關標籤/搜索