一篇圖文完全弄懂Class文件是如何被加載進JVM的

不管你是跟同事、同窗、上下級、同行、或者面試官討論技術問題的時候,很容易捲入JVM大型撕逼現場。爲了可以讓你們從大型撕逼現場中脫穎而出,最近我苦思冥想如何把知識點儘量呈現的容易理解,方便記憶。因而就開啓了這一系列文章的編寫。爲了讓JVM相關知識點可以造成一個體系,arthinking將編寫整理一系列的專題,以儘可能以圖片的方式描述相關知識點,而且最終把全部相關知識點串成了一張圖。持續更新中,歡迎你們閱讀。有任何錯落之處也請您高擡貴手幫忙指正,感謝!html

導讀

一個Class文件,在加載進JVM的過程當中,究竟經歷了些什麼?加載進JVM以後又會以什麼樣的形式呈現?看文本文,你能夠了解到:java

  1. Class.forName到底是怎麼獲取Class對象的,Class對象又是什麼?
  2. Class文件是如何被加載到JVM裏面的?
  3. 類變量是存在堆中仍是存在方法區中?
  4. 類構造器<clinit>方法何時執行?

關於類加載器

Java後端技術架構 · 技術專題 · 經驗分享面試

一、加載一個Class文件

如下是類的生命週期:算法

image-20200104201614588

其中,若是是動態綁定或者晚期綁定,解析階段不會再準備階段後馬上執行。接下來咱們就來看看是如何按照這個流程加載一個Class文件的。數據庫

思考:編程

1.有以下代碼:後端

public class TestLoadSubClass {
    public static void main(String[] args) {
        System.out.println(B.value);
    }
}

class A {
    static {
        System.out.println("init A ...");
    }
    static int value = 100;
  	static final String DESC = "test";
}

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

猜猜會不會輸出 init B緩存

2.猜猜如下語句會不會輸出 init Abash

A[] arrays = new A[10];
複製代碼

3.猜猜如下代碼會不會輸出 init A網絡

System.out.println(A.DESC);
複製代碼

1.一、加載階段

image-20200104234319822

JVM規範並無規定java.lang.Class類的實例要放到Java堆中,對於HotSpot虛擬機,是放到方法區裏面的。這個class對象做爲程序訪問方法區中的這些類型數據的外部接口。

如上圖,加載階段主要作如下事情:

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

1.1.一、如何觸發加載Class文件

如上圖,當如下任何一種狀況發生的時候,會觸發加載Class文件:

  • 遇到new、getstatic、putstatic或者invokestatic字節碼指令的時候,若是類尚未初始化。對應場景爲:
    • new一個對象;
    • 讀取或者設置一個類的靜態字段;
    • 調用類的靜態方法的時候;
  • 使用java.lang.reflect包的方法對類進行反射的時候,若是類尚未初始化;
  • 初始化類的時候,若是父類尚未初始化,則觸發父類初始化;
  • 虛擬機器啓動時,main方法所在的類會首先進行初始化;
  • JDK1.7中使用動態語言支持的時候,若是一個java.lang.invoke.MethodHandler實例最後解析爲:REF_getStatic,REF_putStatic,REF_invokeStatic方法句柄的時候,而且句柄所對應的類沒有進行過初始化。

這個時候經過類的全限定名稱獲取類的二進制字節流。

此時這個字節流爲靜態存儲結構,須要轉換爲方法區的運行時數據結構。結構如上圖方法區中所示。每一個類生成一個對應的結構,結構裏面的信息詳細介紹參考此文:The Java Virtual Machine

其中:

ClassLoader的引用指的是加載這個Class文件的ClassLoader實例的引用;

Class實例引用指的是類加載器在加載類信息並放到方法區以後,而後建立對應的Class類型的實例,並把該實例的引用保存到Class實例引用中。

1.1.二、獲取二進制流的方式

如上圖描述的,JVM規範5.3. Creation and Loading並無指定class文件二進制流須要從哪裏以什麼方式獲取,目前主要有如下幾種獲取方式:

  • zip包,延伸爲JAR、EAR、WAR包;
  • 網絡,如Applet;
  • 動態代理;
  • JSP生成;
  • 數據庫獲取;

1.1.三、驗證二進制字節流

如上圖所示,在加載階段就已經開始作部分驗證工做了,可是驗證仍是屬於鏈接階段的動做,下面介紹驗證階段。

1.二、鏈接階段

image-20200105102811544

如上圖:鏈接階段包括:驗證,準備,解析

1.2.一、驗證階段

驗證階段作什麼事情

爲了解釋這一步的做用,咱們先來作一個實驗。

有以下一個類:

package com.itzhai.jvm.loadclass;

/** * Created by arthinking on 4/1/2020. */
public class TestVerify {

    public static void main(String[] args) {
        System.out.println("Hello world !!!");
    }
}
複製代碼

咱們把Java文件編譯爲class文件,並執行之:

java com.itzhai.jvm.loadclass.TestVerify

能夠發現輸出:

Hello world !!!
複製代碼

如今咱們使用前面Class文件16進制背後的祕密介紹的十六進制編輯方法,對class文件進行隨意編輯,這裏咱們能夠把常量池計數器故意調小一點,保存以後再次執行class文件:

Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Invalid constant pool index 33 in class file com/itzhai/jvm/loadclass/TestVerify
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
複製代碼

能夠發現拋出了異常:非法的常量池索引33,這正是驗證階段乾的事情。

驗證階段幹什麼事情

咱們知道,class文件是能夠被認爲篡改的,虛擬機若是直接拿來執行,可能會把系統給搞崩潰了,因此必定要先對Class文件作嚴格的驗證。驗證階段主要完成如下檢測動做:

1.2.1.一、文件格式驗證

主要按照Class文件16進制背後的祕密文章中的闡述的格式,嚴格的進行校驗。

1.2.1.二、元數據驗證

主要是語義校驗,保證不存在不符合Java語言規範的元數據信息,如:沒有父類,繼承了final類,接口的非抽象類實現沒有完整實現方法等。

1.2.1.三、字節碼驗證

主要對數據流和控制流進行分析,肯定成行語義是否合法,符合邏輯。不合法的例子:

  • 操做數棧放置了int類型數據,卻當成long類型使用;
  • 把父類對象賦值給了子類數據類型;
  • ...

1.2.1.四、符號引用驗證

解析階段發生的驗證,當把符號引用轉化爲直接引用的時候進行驗證。這主要是對類自身之外的信息進行匹配性校驗。主要包括:

  • 全限定名是否能夠找到對應的類;
  • 指定類是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段;
  • 校驗類,字段和方法的可見性;

1.2.二、準備階段

這個階段還並無開始執行類的構造方法,而只是爲類變量分配內存並設置類變量初始值(零值)。這些變量所使用的內存都將在方法區中分配。

基本數據類型的零值:2.3. Primitive Types and Values

這裏只分配static變量,不包括實例變量。

注意:static final類型的常量value會在準備階段被初始化爲常量指定的值。

靜態變量存儲在內存的PremGen(方法區域)空間中,其值存儲在Heap中

1.2.三、解析階段

解析階段主要將常量池內的符號引用替換爲直接引用。

**符號引用:**字面量,引用目標不必定已經加載到內存中;

**直接引用:**直接指向目標的指針,或者相對偏移量,或是一個能簡介定位到目標的句柄。直接引用和虛擬機實現的內存佈局相關。

**關於動態語言的支持:**經過invokedynamic指令支持動態語言。該指令會對符號引用進行解析,可是不會緩存解析的結果,每次執行指令都須要從新解析。

解析主要針對如下七類符號引用進行:

  • 類或接口 CONSTANT_Class_info
  • 字段 CONSTANT_Fieldref_info
  • 類方法 CONSTANT_Methodref_info
  • 接口方法 CONSTANT_InterfaceMethodref_info
  • 方法類型 CONSTANT_MethodType_info
  • 方法句柄 CONSTANT_MethodHandle_info
  • 調用限定符 CONSTANT_InvokeDynamic_info

常量池中的14種常量結構

符號引用解析的過程或校驗的過程當中,可能又會觸發另外一個類的加載。

1.三、初始化階段

image-20200105112555664

這階段開始執行Java程序代碼,這一步主要是執行類構造器<clinit>方法對類變量進行初始化的過程,注意,這個方法不是構造方法。

下面就來介紹一下這個方法:

1.3.一、<clinit>方法

此方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的方法,主要是給類變量作初始化工做的方法。

生成<clinit>方法的實例

有以下代碼:

public class TestInit {

    static {
        DESC = "hello world!!!";
    }

    private static String DESC;

    public void test() {
        DESC = "a";
    }

    public static void main(String[] args) {
        System.out.println(DESC);
    }

}
複製代碼

這個類中有一個靜態變量DESC,而且在靜態代碼塊中進行了賦值操做,咱們看看其生成的彙編代碼:

Constant pool:
   #1 = Methodref          #8.#26         // java/lang/Object."<init>":()V
   #2 = String             #27            // a
   #3 = Fieldref           #7.#28         // com/itzhai/classes/TestInit.DESC:Ljava/lang/String;
   ...
   #7 = Class              #34            // com/itzhai/classes/TestInit
   ...
   #9 = Utf8               DESC
   #10 = Utf8               Ljava/lang/String;
   ...
   #23 = Utf8               <clinit>
   #28 = NameAndType        #9:#10         // DESC:Ljava/lang/String;
   ...

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #6                  // String hello world!!!
         2: putstatic     #3                  // Field DESC:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
複製代碼

能夠發現,生成了這樣的一個方法。此方法既是生成的<clinit>方法。這裏指令比較簡單,主要是:拿到」hello world!!!「字符串的引用,把他設置到DESC類變量中。

關於<clinit>方法的注意事項

  • **順序問題:**靜態語句塊後面的靜態變量,靜態語句塊中能夠賦值,但不能夠訪問;
    • image-20200105105332563
  • **繼承執行順序:**無需顯示調用,虛擬機會保證子類的<clinit>方法執行前,父類的<clinit>方法已經執行完畢;
  • **接口的<clinit>方法:**雖然接口不能有靜態語句塊,可是能夠給靜態變量初始化值,因此也能夠生成<clinit>方法;
  • **接口繼承:**除非使用到父接口的變量,不然執行子接口的<clinit>方法不須要先執行父接口的<clinit>方法;
  • 在併發場景,虛擬機會保證一個類的<clinit>方法只有一個線程執行,其餘線程會阻塞,因此要確保靜態代碼塊中不要寫可能回到成進程阻塞的代碼。

References

JVM Internals

Where are static methods and static variables stored in Java?

運行時常量池 和 字符串常量池相關:The String Constant Pool

關於方法區中的Class文件信息說明:Chapter 5 of Inside the Java Virtual Machine

《深刻理解Java虛擬機-JVM高級特性與最佳實踐》

Chapter 5. Loading, Linking, and Initializing


本文爲arthinking基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。

你們能夠關注個人博客:itzhai.com 獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。

若是您以爲讀完本文有所收穫的話,能夠關注個人帳號,或者點贊的,您的支持就是我寫做的動力!關注個人公衆號,及時獲取最新的文章。


本文做者: arthinking

博客連接: www.itzhai.com/jvm/how-cla…

一篇圖文完全弄懂Class文件是如何被加載進JVM的

版權聲明: BY-NC-SA許可協議:創做不易,如需轉載,請務必附加上博客連接,謝謝!


相關文章
相關標籤/搜索