【JVM】類加載機制

原文:【深刻Java虛擬機】之四:類加載機制html

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。它們開始的順序以下圖所示:
java

類加載的過程包括了加載、連接、初始化三個階段。連接包括驗證、準備、解析三個部分程序員

在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。面試

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

靜態綁定:即前期綁定。在程序執行前方法已經被綁定,此時由編譯器或其它鏈接程序實現。針對java,簡單的能夠理解爲程序編譯期的綁定。java當中的方法只有final,static,private和構造方法是前期綁定的。數組

動態綁定:即晚期綁定,也叫運行時綁定。在運行時根據具體對象的類型進行綁定。在java中,幾乎全部的方法都是後期綁定的。緩存

下面詳細講述類加載過程當中每一個階段所作的工做。安全

加載

加載是類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:服務器

一、經過一個類的全限定名來獲取其定義的二進制字節流。網絡

二、將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。

三、在java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。

注意:這裏第1條中的二進制字節流並不僅是單純地Class文件中獲取,好比它還能夠從jar包中獲取、從網絡中獲取(最典型的應用即是Applet)、由其餘文件生成(JSP應用)等。若是輸入數據不是 ClassFile 的結構,則會拋出 ClassFormatError

第2條是指JVM分析並將這些二進制數據流轉換爲方法區(JVM的內存結構:方法區、堆,棧,本地方法棧,pc 寄存器)特定的數據結構(這些數據結構是實現有關的,不一樣 JVM 有不一樣實現)。這裏處理了部分檢驗,好比類文件的魔數的驗證,檢查文件是否過長或者太短,肯定是否有父類(除了 Obecjt 類)。

第3條要注意,有了對應的 Class 實例,並不意味着這個類已經完成了加載鏈連接!

相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。

類加載器和雙親委派機制

說到加載,不得不提到類加載器,下面就具體講述下類加載器

類加載器雖然只用於實現類的加載動做,但它在java程序中起到的做用卻遠遠不限於類的加載階段。對於任意一個類,都須要由它的類加載器和這個類自己一同肯定其在就Java虛擬機中的惟一性,也就是說,即便兩個類來源於同一個Class文件,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。這裏的「相等」包括了表明類的Class對象的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對對象所屬關係的斷定結果。所以,即使是一樣的java字節代碼,被兩個不一樣的類加載器定義以後,所獲得的java類也是不一樣的。若是試圖在兩個類的對象之間進行賦值操做,會拋出java.lang.ClassCastException。這個特性爲一樣名稱的java類在JVM中共存創造了條件。在實際的應用中,可能會要求同一名稱的java類的不一樣版本在JVM中能夠同時存在。經過類加載器就能夠知足這種需求。這種技術在OSGI中獲得了普遍的應用。

站在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實現,它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的全部類庫(如javax.*開頭的類),開發者能夠直接使用擴展類加載器。

三、應用程序類加載器:Application ClassLoader
該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器

應用程序都是由這三種類加載器互相配合進行加載的,若是有必要,咱們還能夠加入自定義的類加載器。由於JVM自帶的ClassLoader只是懂得從本地文件系統加載標準的java class文件,所以若是編寫了本身的ClassLoader,即可以作到以下幾點:

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

2)動態地建立符合用戶特定須要的定製化構建類。

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

事實上當使用Applet的時候,就用到了特定的ClassLoader,由於這時須要從網絡上加載java class,而且要檢查相關的安全信息,應用服務器也大都使用了自定義的ClassLoader技術。

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

這種層次關係稱爲類加載器的雙親委派模型。咱們把每一層上面的類加載器叫作當前層類加載器的父加載器,固然,它們之間的父子關係並非經過繼承關係來實現的,而是使用組合關係來複用父加載器中的代碼。該模型在JDK1.2期間被引入並普遍應用於以後幾乎全部的java程序中,但它並非一個強制性的約束模型,而是java設計者們推薦給開發者的一種類的加載器實現方式。

雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。

使用雙親委派模型來組織類加載器之間的關係,有一個很明顯的好處,就是java類隨着它的類加載器(說白了,就是它所在的目錄)一塊兒具有了一種帶有優先級的層次關係,這對於保證Java程序的穩定運做很重要。例如,類java.lang.Object類存放在JDK\jre\lib下的rt.jar之中,所以不管是哪一個類加載器要加載此類,最終都會委派給啓動類加載器進行加載,這邊保證了Object類在程序中的各類類加載器中都是同一個類。

連接

java類的連接指的是將java類的二進制代碼合併到JVM的運行狀態之中的過程。在連接以前,這個類必須被成功加載。

第二階段是連接(Linking),這是核心的步驟,連接的過程比加載過程要複雜不少,這是實現java的動態性的重要一步!簡單說是把原始的類定義信息平滑地轉化入 JVM 運行的過程當中。這裏可進一步細分爲三個步驟:

驗證(Verification
這是虛擬機安全的重要保障,JVM 須要覈驗字節信息是符合 java 虛擬機規範的,換言之,驗證是用來確保Java類的二進制表示在結構上是徹底正確的,不然就被認爲是 java.lang.VerifyError,這樣就防止了惡意信息或者不合規的信息危害 JVM 的運行,驗證階段有可能觸發更多 class 的加載。

準備(Preparation
建立類或接口中的靜態變量,並初始化靜態變量的初始值。但這裏的「初始化」和下面的顯式初始化階段是有區別的,側重點在於分配所須要的內存空間,不會去執行更進一步的 JVM 指令。

對類的成員變量分配空間。雖然有初始值,但這個時候不會對他們進行初始化(由於這裏不會執行任何 java 代碼)。具體以下:
全部原始類型的值都爲 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底層實現大多使用 int),引用類型則爲 null。值得注意的是,JVM 可能會在這個時期給一些有助於程序運行效率提升的數據結構分配空間。

【JVM】關於類加載器準備階段的一道面試題目

解析(Resolution
在這一步會將常量池中的符號引用(symbolic reference)替換爲直接引用。在java 虛擬機規範中,詳細介紹了類、接口、方法和字段等各個方面的解析。

在一個java類中會包含對其它類或接口的形式引用,包括它的父類、所實現的接口、方法的形式參數和返回值的java類等。解析的過程就是確保這些被引用的類能被正確的找到。解析的過程可能會致使其它的java類被加載。

爲類、接口、方法、成員變量的符號引用定位直接引用(若是符號引用先到常量池中尋找符號,再找先應的類型,無疑會耗費更多時間),完成內存結構的佈局。

這一步是可選的。能夠在符號引用第一次被使用時完成,即所謂的延遲解析(late resolution)。但對用戶而言,這一步永遠是延遲解析的,即便運行時會執行 early resolution,但程序不會顯示的在第一次判斷出錯誤時拋出錯誤,而會在對應的類第一次主動使用的時候拋出錯誤!

另外,這一步與以後的類初始化是不衝突的,並不是必定要全部的解析結束之後才執行類的初始化。不一樣的 JVM 實現不一樣。

看下面一段代碼:

public class LinkTest {   
   public static void main(String[] args) {       
      ToBeLinked toBeLinked = null;       
      System.out.println("Test link.");   
   }
}

LinkTest引用了類ToBeLinked,可是並無真正使用它,只是聲明瞭一個變量,並無建立該類的實例或是訪問其中的靜態域。若是把編譯好的ToBeLinkedjava字節代碼刪除以後,再運行LinkTest,程序不會拋出錯誤。這是由於ToBeLinked類沒有被真正用到。連接策略使得ToBeLinked類不會被加載,所以也不會發現ToBeLinkedjava字節代碼其實是不存在的。若是把代碼改爲ToBeLinked toBeLinked = new ToBeLinked();以後,再按照相同的方法運行,就會拋出異常了。由於這個時候ToBeLinked這個類被真正使用到了,會須要加載這個類。

下面對驗證、準備、解析階段進行詳細分析。

驗證

驗證的目的是爲了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。不一樣的虛擬機對類驗證的實現可能會有所不一樣,但大體都會完成如下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

文件格式的驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內。通過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。

元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合java語法規範的元數據信息。

字節碼驗證:該階段驗證的主要工做是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會作出危害虛擬機安全的行爲。

符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。

準備

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

一、這時候進行內存分配的僅包括類變量(static,而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在java堆中。

二、這裏所設置的初始值一般狀況下是數據類型默認的零值(如0、0L、null、false等),而不是被在java代碼中被顯式地賦予的值。

假設一個類變量的定義爲:

public static int value = 3;

那麼變量value在準備階段事後的初始值爲0,而不是3,由於這時候還沒有開始執行任何java方法,而把value賦值爲3putstatic指令是在程序編譯後,存放於類構造器<clinit>()方法之中的,因此把value賦值爲3的動做將在初始化階段纔會執行。

下表列出了java中全部基本數據類型以及reference類型的默認零值:

這裏還須要注意以下幾點:

對基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過。

Ⅱ 對於同時被staticfinal修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過;而只被final修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。

Ⅲ 對於引用數據類型reference來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。

Ⅳ 若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。

三、若是類字段的字段屬性表中存在ConstantValue屬性,即同時被finalstatic修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值。

假設上面的類變量value被定義爲:

public static final int value = 3;

編譯時javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲3能夠理解爲static final常量在編譯期就將其結果放入了調用它的類的常量池中。注意此處與第2點的區別,第2點的例子public static int value = 3;只被static修飾,在準備階段該value只會被初始化爲基本數據類型的默認零值0

解析

解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程。在Class類文件結構一文中已經比較過了符號引用和直接引用的區別和關聯,這裏再也不贅述。前面說解析階段可能開始於初始化以前,也可能在初始化以後開始,虛擬機會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析(初始化以前),仍是等到一個符號引用將要被使用前纔去解析它(初始化以後)?

對同一個符號引用進行屢次解析請求時很常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標示爲已解析狀態),從而避免解析動做重複進行。

解析動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行,分別對應於常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量類型。

一、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,仍是普通的對象類型的引用,從而進行不一樣的解析。
二、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,若是有,則查找結束;若是沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,尚未,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束,查找流程以下圖所示:

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

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改成接口,並將Child類繼承Father類且實現Super接口,那麼在編譯時會報出以下錯誤:

StaticTest.java:24: 對 m 的引用不明確,Father 中的 變量 m 和 Super 中的 變量 m

都匹配
                System.out.println(Child.m);
                                        ^
1 錯誤

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

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

初始化

初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的java程序代碼。

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

在編譯生成的Class文件時,會自動產生兩個方法,一個是類的初始化方法<clinit>,另外一個是實例的初始化方法<init>。能夠簡單理解爲<clinit>是用戶初始化靜態變量和靜態塊的,且只執行一次,而<init>是用於對象實例化的構造器方法,每次對象實例化時都會調用。

一、 <clinit>方法在JVM第一次加載Class文件時調用(因此靜態塊只會執行一次),包括靜態塊的執行和靜態變量初始化
二、<init>方法在實例(對象)建立時候調用, 就是生成對象的時候, 例如new ,反射等等。

這裏簡單說明下<clinit>()方法的執行規則:

一、<clinit>()方法是由編譯器自動收集類中的全部類變量(static)的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句中能夠賦值,可是不能訪問。

二、<clinit>()方法與實例構造器<init>()方法(類的構造函數)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢,因此父類的靜態塊等要優先於子類的操做。所以,在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object

三、<clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。

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

五、虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。

下面給出一個簡單的例子,以便更清晰地說明如上規則:

class Father{  
    public static int a = 1;  
    static{  
        a = 2;  
    }  
}  
  
class Child extends Father{  
    public static int b = a;  
}  
  
public class ClinitTest{  
    public static void main(String[] args){  
        System.out.println(Child.b);  
    }  
}

執行上面的代碼,會打印出2,也就是說b的值被賦爲了2

咱們來看獲得該結果的步驟。
一、首先在準備階段類變量分配內存並設置類變量初始值,這樣AB均被賦值爲默認值0
二、然後再在調用<clinit>()方法時(初始化階段),給他們賦予程序中指定的值。當咱們調用Child.b時,觸發Child的<clinit>()方法,根據規則2,在此以前,要先執行完其父類Father<clinit>()方法,又根據規則1,在執行<clinit>()方法時,須要按static語句或static變量賦值操做等在代碼中出現的順序來執行相關的static語句,所以當觸發執行Father<clinit>()方法時,會先將a賦值爲1,再執行static語句塊中語句,將a賦值爲2,然後再執行Child類的<clinit>()方法,這樣便會將b的賦值爲2

若是咱們顛倒一下Father類中public static int a = 1;語句static語句塊的順序,程序執行後,則會打印出1

class Father{  

    static{  
        a = 2;  
    }
    
    public static int a = 1;//與上面的靜態代碼塊調換了位置
}

很明顯是根據規則1,執行Father<clinit>()方法時,根據順序先執行了static語句塊中的內容,後執行了public static int a = 1;語句。

另外,在顛倒兩者的順序以後,若是在static語句塊中對a進行訪問(好比將a賦給某個變量),在編譯時將會報錯,由於根據規則1,它只能對a進行賦值,而不能訪問。

JVM初始化補充說明

類的初始化也是延遲的,直到類第一次被主動使用(active use),JVM纔會初始化類。

當一個java類第一次被真正使用到的時候,JVM會進行該類的初始化操做。初始化過程的主要操做是執行靜態代碼塊和初始化靜態域。在一個類被初始化以前,它的直接父類也須要被初始化。可是,一個接口的初始化,不會引發其父接口的初始化。在初始化的時候,會按照源代碼中從上到下的順序依次執行靜態代碼塊和初始化靜態域。(具體規則參見上面<clinit>()方法的執行規則)

public class StaticTest {   
   public static int X = 10;   
   public static void main(String[] args) {       
      System.out.println(Y); //輸出60   
   }   
   static {       
      X = 30;   
   }  
   public static int Y = X * 2;
}

在上面的代碼中,在初始化的時候,靜態域的初始化和靜態代碼塊的執行會從上到下依次執行。所以變量X的值首先初始化成10,後來又被賦值成30;而變量Y的值則被初始化成60

類的初始化分兩步:

1.若是基類沒有被初始化,初始化基類。
2.有類構造函數,則執行類構造函數。

類構造函數是由 java 編譯器完成的。它把類成員變量的初始化和 static 區間的代碼提取出,放到一個<clinit>方法中。這個方法不能被通常的方法訪問(注意,static final 成員變量不會在此執行初始化,它通常被編譯器生成 constant 值,在準備階段第3點有說明,準備階段就會對static final 常量初始化)。同時,<clinit>中是不會顯示的調用基類的<clinit>的,由於 1 中已經執行了基類的初始化(詳細規則在上面<clinit>()方法的執行規則第2點)。該初始化過程是由 JVM 保證線程安全的。

java類和接口的初始化只有在特定的時機纔會發生,這些時機包括:

一、建立一個java類的實例。如

MyClass obj = new MyClass()

二、調用一個java類中的靜態方法。如

MyClass.sayHello()

三、給java類或接口中聲明的靜態域賦值。如

MyClass.value = 10

四、訪問java類或接口中聲明的靜態域,而且該域不是常值變量。如

int value = MyClass.value

五、在頂層java類中執行assert語句。

經過 java 反射 API 也可能形成類和接口的初始化。須要注意的是,當訪問一個java類或接口中的靜態域的時候,只有真正聲明這個域的類或接口才會被初始化。考慮下面的代碼:

class B {   
   static int value = 100;   
   static {       
      System.out.println("Class B is initialized."); //輸出   
   }
}
class A extends B {   
   static {       
      System.out.println("Class A is initialized."); //不會輸出   
   }
}
public class InitTest {   
   public static void main(String[] args) {       
      System.out.println(A.value); //輸出100   
   }
}

在上述代碼中,類InitTest經過A.value引用了類B中聲明的靜態域value。因爲value是在類B中聲明的,只有類B會被初始化,而類A則不會被初始化。

總結

整個類加載過程當中,除了在加載階段用戶應用程序能夠自定義類加載器參與以外,其他全部的動做徹底由虛擬機JVM主導和控制。

到了初始化纔開始執行類中定義的java程序代碼(即字節碼),但這裏的執行代碼只是個開端,它僅限於<clinit>()方法。

類加載過程當中主要是將Class文件(準確地講,應該是類的二進制字節流)加載到虛擬機內存中,真正執行字節碼的操做,在加載完成後才真正開始。

對於某個特定的類加載器來講,一個 java類只能被載入一次,也就是說在JVM中,類的完整標識是(classLoader,package,className)。

另一個類能夠被不一樣的類加載器加載,可是,同一個類,由不一樣的類加載器實例加載的話,會在方法區產生兩個不一樣的類,彼此不可見,而且在堆中生成不一樣 Class實例。

若是使用的類加載器沒有重寫ClassLoader中的getSystemClassLoader方法,那麼使用的都是AppClassLoader的同一個實例進行類的加載的,因此加載同一個類在內存中只有一個類,即全部經過正常雙親委派模式的類加載器加載的classpathext下的全部類在方法區都是同一個類,中的Class實例也是同一個。

參考資料:
Java類的加載、連接和初始化
自定義類加載器加載類遇到的問題
關於static的加載順序
極客時間:第23講 | 請介紹類加載過程,什麼是雙親委派模型?

相關文章
相關標籤/搜索