Java虛擬機之Java類的加載

?? Java 虛擬機中的類加載,按先后順序需要經(jīng)過加載、鏈接以及初始化三大步驟。
其中,鏈接過程中同樣需要驗證;而內(nèi)存中的類沒有經(jīng)過初始化,同樣不能使用。那么,是否所有的 Java 類都需要經(jīng)過這幾步呢
在Java語言中,類型可以分為兩大類:基本類型與引用類型。其中基本類型都是有Jav虛擬機預(yù)先定義好的。而引用類型又可以細(xì)分成四種:類,接口,數(shù)組類和泛型參數(shù)。由于泛型參數(shù)在編譯過程中會被擦除,所以,對于Java虛擬機來說,實際上只有前三種類型。在類、接口和數(shù)組中,數(shù)組類是由Java虛擬機直接生成的,其他兩種則有對應(yīng)的字節(jié)流。字節(jié)流(也就是java編譯器編譯成的class文件)都會被加載到j(luò)ava虛擬機中,成為類或者接口(以下統(tǒng)稱類)。
但是,無論是虛擬機直接生成的數(shù)組類,還是加載的類,java虛擬機都需要對其進(jìn)行鏈接和初始化。

加載

?? 加載,就是指查找字節(jié)流,并且創(chuàng)建類的過程。對于數(shù)據(jù)類來說,他沒有對應(yīng)字節(jié)流,由java虛擬機直接生成。對于其他類來說,就需要java虛擬機借助類加載器來完成字節(jié)流查找的過程。
?? 關(guān)于類加載器主要分為三種,啟動類加載器,擴展類加載器以及應(yīng)用類加載器。在java虛擬機中,每當(dāng)有一個類加載器接收到加載請求是,它會先將請求轉(zhuǎn)發(fā)給父類加載器,在父類加載器沒有找到所請求的類的情況下,該類加載器才會嘗試去加載。這樣的加載方式我們稱之為雙親委派模型。
?? 在Java9之前,啟動類加載器負(fù)責(zé)加載最為基礎(chǔ)、最為重要的類(JRE的lib目錄下jar包中的類)。擴展類加載器的父類加載器是啟動類加載器,負(fù)責(zé)加載次要的但又通用的類(JRE的lib/ext目錄下jar包中的類)。最后的應(yīng)用類加載器的父類加載器是擴展類加載器,負(fù)責(zé)加載應(yīng)用程序路徑下的類。java9之后,擴展類加載器更名為平臺類加載器,除了少數(shù)幾個關(guān)鍵模塊由啟動類加載器加載外,其他模塊均由平臺類加載器所加載。

鏈接

?? 鏈接,是指將創(chuàng)建成的類合并至Java虛擬機中,使之能夠執(zhí)行的過程。它可分為驗證、準(zhǔn)備以及解析三個階段。
?? 驗證:驗證的目的在于被加載的類能夠滿足Java虛擬機的規(guī)范。通常來說,通過Java編譯器生成的類文件一定滿足Java虛擬機的規(guī)范。(如果存在編譯成字節(jié)碼之后,反匯編修改了字節(jié)碼的情況??赡芫筒粷M足Java虛擬機規(guī)范)
?? 準(zhǔn)備:準(zhǔn)備階段則是為被加載的類的靜態(tài)字段分配內(nèi)存。Java代碼中對靜態(tài)字段的具體初始化,則會在接下來的初始化階段中完成。當(dāng)然,準(zhǔn)備階段除了會為靜態(tài)字段分配內(nèi)存之外,有些虛擬機還會例如實現(xiàn)虛方法的動態(tài)綁定的方法表等等。
?? 解析:在class文件被加載至Java虛擬機之前,這個類無知道其他類以及其方法、字段所對應(yīng)的具體地址,甚至也不知道自己方法、字段的地址。所以,Java編譯器在遇到需要引用成員時,會為其生成一個符號引用。在運行階段,就可以通過該符號引用無歧義地定位到具體的目標(biāo)之上。而解析階段的目的就是講這些符號引用解析成為實際地址引用。當(dāng)然,如果符號引用指向了一個未被加載的類,或者未被加載的字段、方法,那么解析將會觸發(fā)這個類的加載(但不一定會觸發(fā)這個類的鏈接以及初始化的過程)
?? PS:Java虛擬機規(guī)范中并沒有要求在鏈接過程中完成解析。它僅規(guī)定了:如果某些字節(jié)碼使用了符號引用,那么在執(zhí)行這些字節(jié)碼之前,需要對這些字節(jié)碼符號引用進(jìn)行解析。

初始化

?? 類加載的最后一步便是初始化了。在Java代碼中,如果靜態(tài)字段被final修飾,并且它的類型是基本類型或字符串時,那么該字段便會被Java編譯器標(biāo)記成常量值,其初始化過程由Java虛擬機直接完成。除此之外的賦值操作,以及所有靜態(tài)代碼塊中的代碼,則會被Java編譯器優(yōu)化到同一方法中,并把它命名為 <clinit >。初始化完成之后,類便成為可執(zhí)行的狀態(tài)。
關(guān)于類的初始化觸發(fā)條件:
1.當(dāng)虛擬機啟動時,初始化用戶指定的主類;
2.當(dāng)遇到用以新建目標(biāo)類實例的new指令時,初始化new指令的目標(biāo)類;
3.當(dāng)遇到調(diào)用靜態(tài)方法的指令時,初始化該靜態(tài)方法所在的類;
4.當(dāng)遇到訪問靜態(tài)字段的指令時,初始化該靜態(tài)字段所在的類;
5.子類的初始化會觸發(fā)父類的初始化;
6.使用反射API對某個類進(jìn)行反射調(diào)用時,初始化這個類;

實例操作
新建Singleton.java

public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
    static {
      System.out.println("LazyHolder.<clinit>");
    }
  }
  public static Object getInstance(boolean flag) {
    if (flag) return new LazyHolder[2];
    return LazyHolder.INSTANCE;
  }
  public static void main(String[] args) {
    getInstance(true);
    System.out.println("----");
    getInstance(false);
  }
} 

運行命令
javac Singleton.java
java -verbose:class Singleton
PS:-verbose:class可以打印類加載的先后順序

image.png

從運行結(jié)果可以看出調(diào)用getInstance(true)時,也就是新建LazyHolder數(shù)組時,只加載的類,并沒有初始化。初始化是在調(diào)用getInstance(false)時完成。同時也可以通過openjdk工具包中下載asmtools.jar工具來修改字節(jié)碼,得出在修改了編譯后的字節(jié)碼后,調(diào)用getInstance(true)時也不會調(diào)用鏈接的過程(因為鏈接的第一步就是驗證字節(jié)碼是否符合JVM規(guī)范).
總結(jié):新建引用類型類的數(shù)組時,只會加載,不會鏈接和初始化。鏈接和初始化在真正實例化的時候會觸發(fā)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容