不管你是跟同事、同窗、上下級、同行、或者面試官討論技術問題的時候,很容易捲入JVM大型撕逼現場。爲了可以讓你們從大型撕逼現場中脫穎而出,最近我苦思冥想如何把知識點儘量呈現的容易理解,方便記憶。因而就開啓了這一系列文章的編寫。爲了讓JVM相關知識點可以造成一個體系,arthinking將編寫整理一系列的專題,以儘可能以圖片的方式描述相關知識點,而且最終把全部相關知識點串成了一張圖。持續更新中,歡迎你們閱讀。有任何錯落之處也請您高擡貴手幫忙指正,感謝!html
一個Class文件,在加載進JVM的過程當中,究竟經歷了些什麼?加載進JVM以後又會以什麼樣的形式呈現?看文本文,你能夠了解到:java
<clinit>
方法何時執行?Java後端技術架構 · 技術專題 · 經驗分享面試
如下是類的生命週期:算法
其中,若是是動態綁定或者晚期綁定,解析階段不會再準備階段後馬上執行。接下來咱們就來看看是如何按照這個流程加載一個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); 複製代碼
JVM規範並無規定java.lang.Class類的實例要放到Java堆中,對於HotSpot虛擬機,是放到方法區裏面的。這個class對象做爲程序訪問方法區中的這些類型數據的外部接口。
如上圖,加載階段主要作如下事情:
如上圖,當如下任何一種狀況發生的時候,會觸發加載Class文件:
java.lang.reflect
包的方法對類進行反射的時候,若是類尚未初始化;這個時候經過類的全限定名稱獲取類的二進制字節流。
此時這個字節流爲靜態存儲結構,須要轉換爲方法區的運行時數據結構。結構如上圖方法區中所示。每一個類生成一個對應的結構,結構裏面的信息詳細介紹參考此文:The Java Virtual Machine
其中:
ClassLoader的引用
指的是加載這個Class文件的ClassLoader實例的引用;
Class實例引用
指的是類加載器在加載類信息並放到方法區以後,而後建立對應的Class類型的實例,並把該實例的引用保存到Class實例引用中。
如上圖描述的,JVM規範5.3. Creation and Loading並無指定class文件二進制流須要從哪裏以什麼方式獲取,目前主要有如下幾種獲取方式:
如上圖所示,在加載階段就已經開始作部分驗證工做了,可是驗證仍是屬於鏈接階段的動做,下面介紹驗證階段。
如上圖:鏈接階段包括:驗證,準備,解析
爲了解釋這一步的做用,咱們先來作一個實驗。
有以下一個類:
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文件作嚴格的驗證。驗證階段主要完成如下檢測動做:
主要按照Class文件16進制背後的祕密文章中的闡述的格式,嚴格的進行校驗。
主要是語義校驗,保證不存在不符合Java語言規範的元數據信息,如:沒有父類,繼承了final類,接口的非抽象類實現沒有完整實現方法等。
主要對數據流和控制流進行分析,肯定成行語義是否合法,符合邏輯。不合法的例子:
解析階段發生的驗證,當把符號引用轉化爲直接引用的時候進行驗證。這主要是對類自身之外的信息進行匹配性校驗。主要包括:
這個階段還並無開始執行類的構造方法,而只是爲類變量分配內存並設置類變量初始值(零值)。這些變量所使用的內存都將在方法區中分配。
基本數據類型的零值:2.3. Primitive Types and Values
這裏只分配static變量,不包括實例變量。
注意:static final類型的常量value會在準備階段被初始化爲常量指定的值。
靜態變量存儲在內存的PremGen(方法區域)空間中,其值存儲在Heap中
解析階段主要將常量池內的符號引用替換爲直接引用。
**符號引用:**字面量,引用目標不必定已經加載到內存中;
**直接引用:**直接指向目標的指針,或者相對偏移量,或是一個能簡介定位到目標的句柄。直接引用和虛擬機實現的內存佈局相關。
**關於動態語言的支持:**經過invokedynamic指令支持動態語言。該指令會對符號引用進行解析,可是不會緩存解析的結果,每次執行指令都須要從新解析。
解析主要針對如下七類符號引用進行:
符號引用解析的過程或校驗的過程當中,可能又會觸發另外一個類的加載。
這階段開始執行Java程序代碼,這一步主要是執行類構造器<clinit>
方法對類變量進行初始化的過程,注意,這個方法不是構造方法。
下面就來介紹一下這個方法:
<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>
方法的注意事項<clinit>
方法執行前,父類的<clinit>
方法已經執行完畢;<clinit>
方法:**雖然接口不能有靜態語句塊,可是能夠給靜態變量初始化值,因此也能夠生成<clinit>
方法;<clinit>
方法不須要先執行父接口的<clinit>
方法;<clinit>
方法只有一個線程執行,其餘線程會阻塞,因此要確保靜態代碼塊中不要寫可能回到成進程阻塞的代碼。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…
版權聲明:
BY-NC-SA
許可協議:創做不易,如需轉載,請務必附加上博客連接,謝謝!