DAY5:你必須知道的java虛擬機之類篇——類文件的加載(1)

NO IMAGE

過完年有點翻水水,幾天不敲代碼手指頭都不聽使喚了,不知道大家有沒有和我一樣的感覺~~

通過上面幾個篇幅的講解,我們已經完成了虛擬機運行時數據區域、對象的創建過程、class文件的文件結構的學習,並完成了一個完整的class文件的全解析過程。接下來相繼會圍繞類的加載過程、常用的gc收集器等內容來展開。

本章提要

本章分為兩小節主要的內容是圍繞類加載的過程來展開,從類加載的各個階段的職責,到雙親委派模型,並通過重寫loadclss()和findclass()來實戰加深印象。

類加載的過程

類的加載一共是7個過程,分別是加載、驗證、準備、解析、初始化、使用和卸載,不知道大家還記不記得對象創建的7個過程呢?這兩個可以放一起來記。
DAY5:你必須知道的java虛擬機之類篇——類文件的加載(1)

由於java語言的動態綁定的性質,所以解析階段可能在初始化之前,也可能發生在初始化之後進行。

《Java虛擬機規範》中對初始化階段的把控有嚴格的規定,一共是6中情況下,類必須提前完成初始化,不然就會報錯。

1. 遇到new、getstatic、putstatic或invokestatic這四條字節碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段。

2. 使用java.lang.reflect包的方法對類型進行反射調用的時候,如果類型沒有進行過初始化,則需要先觸發其初始化。

3. 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化(接口是沒有這一點要求的,只有在使用到父類接口的時候才會觸發父類接口的初始化)

4.當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

5.當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句柄,並且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化。

6. 當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。

對於這六種會觸發類型進行初始化的場景,《Java虛擬機規範》中使用了一個非常強烈的限定語
——“有且只有”,這六種場景中的行為稱為對一個類型進行主動引用。除此之外,所有引用類型的方
式都不會觸發初始化,稱為被動引用。

來看下被動引用的3中場景

新建3個類備用

public class SuperClass {
static {
System.out.println("父類靜態塊執行");
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static {
System.out.println("子類靜態塊執行");
}
}
public class ConstClass {
static {
System.out.println("常量類靜態塊執行");
}
public static final String HELLOWORLD = "hello world";
}

然後在main中進行調用,通過靜態塊中的代碼是否有打印可以判斷出該類是否被初始化

  public static void main(String[] args) {
//引用父類的靜態字段不會導致子類初始化
System.out.println(SubClass.value);
//通過數組定義來引用類,不會觸發此類的初始化
SuperClass[] sca = new SuperClass[10];
//常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
System.out.println(ConstClass.HELLOWORLD);
}

調用結果如下:

父類靜態塊執行
123
hello world

可以看到自始至終只有父類也就是SuperClass的靜態快內容輸出了,所以說以上三種場景的引用都是被動引用,不會觸發當前類的初始化。

(1) 加載

加載階段主要完成三件事,之後我們要說的類加載器也就是來完成這個過程而誕生的。

1.通過一個類的全限定名來獲取定義此類的二進制字節流。

2.將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

3.在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入
口。

加載階段數組的加載稍微有些特別,因為數組本身是由jvm創建出來的,不是通過類加載器加載出來的,所以還是有幾點是需要注意的。

1.如果數組的組件類型是引用類型,那麼就通過傳統的類加載過程來加載這個組件類型

2.如果數組內部是基本數據類型類似int[] 那麼jvm會把數組標記為與引導類加載器關聯,也就是BootStrap

3.數組的可訪問性和組件的可訪問性一致,基本數據類型的可訪問性默認是public

(2) 驗證

java語言本身是相對安全的編程語言,純代碼是很難作出諸如訪問數組邊界以外的數據,但是字節碼文件不同,字節碼文件作為平臺無關性的基礎,不止是java,別的語言也都是能轉換成字節碼文件的,所以java無法完成的不代表字節碼無法做到,所以為了保護jvm的安全,驗證字節碼文件是必須的。

1.文件格式驗證

文件格式驗證要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。驗證的項目可以參考之前寫的類文件結構

2.元數據驗證

元數據驗證的主要目的是對類的元數據信息進行語義校驗,保證不存在與《Java語言規範》定義相
悖的元數據信息。

3.字節碼驗證

字節碼驗證是整個驗證過程中最複雜的一個階段,主要目的是通過數據流分析和控制流分析,確定
程序語義是合法的、符合邏輯的

4.符號引用驗證

最後一個階段的校驗行為發生在虛擬機將符號引用轉化為直接引用[3]的時候,這個轉化動作將在
連接的第三階段——解析階段中發生。符號引用驗證可以看作是對類自身以外(常量池中的各種符號
引用)的各類信息進行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部
類、方法、字段等資源。主要目的是確保解析行為能正常執行,如果無法通過符號引用驗證,Java虛擬機將會拋出一個java.lang.IncompatibleClassChangeError的子類異常,典型的如:
java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等

(3) 準備

準備階段是正式為類中定義的變量(即靜態變量,被static修飾的變量)分配內存並設置類變量初
始值的階段。這裡的初始值指的是變量的默認值
DAY5:你必須知道的java虛擬機之類篇——類文件的加載(1)

這裡需要注意的一點是,準備階段只針對變量進行默認值的設定,如果是常量,那麼將會從常量池直接讀取數值。

//是一個靜態變量,準備階段value = 0
public static int value = 123;
//是一個靜態常量,所以準備階段讀取常量池的數值,value = 123
public static final int value = 123;

(4) 解析

解析階段是Java虛擬機將常量池內的符號引用替換為直接引用的過程。

解析過程就之前寫的類文件結構解析的過程,大家可以回去回顧一下字節碼文件是如何一步一步地完成解析的。

(5) 初始化

初始化階段就是執行類構造器clinit()方法的過程。
clinit()方法由是什麼呢?

1.clinit()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問
到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問

2.clinit()方法與類的構造函數(即在虛擬機視角中的實例構造器init()方法)不同,它不需要顯式地調用父類構造器,Java虛擬機會保證在子類的clinit()方法執行前,父類的clinit()方法已經執行完畢。因此在Java虛擬機中第一個被執行的clinit()方法的類型肯定是java.lang.Object。

3.由於父類的clinit()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變量賦值
操作

4.接口也有變量賦值的操作所以clinit()方法也是會生成的,但是就像前面說過的,接口的初始化和類的初始化不同,接口的初始化不要求父類的clinit()方法先執行。

5.java虛擬機在多線程環境下,多個線程同時去初始化一個類,只有一個線程來執行clinit()方法,當一個類完成初始化之後,之後的線程就再去執行初始化機會判斷已經存在來初始化的類,就會直接去取已經被初始化之後的那個類,而不會初始化兩次

這裡用一個例子來證明一下

public class ThreadsInit {
static CountDownLatch countDownLatch = new CountDownLatch(6);
static class Parent {
static {
try {
System.out.println(System.currentTimeMillis() + "---Parent類被初始化");
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 6; i++) {
new Thread(() -> {
new Parent();
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("main函數結束");
}
}

輸出結果:

1614149302325---Parent類被初始化
main函數結束

發現parent類始終只會初始化一次。


這一小節主要是介紹了類加載的過程具體是哪幾個,每一個過程的職責是什麼,下一小節會圍繞類加載這一過程展開,著重在類加載器、雙親委派模型和“破壞”雙親委派模型這三個點。

大家看完了別忘了點點贊👍呀~~新的一年祝大家牛轉乾坤,喜事連連。

相關文章

教你寫出高性能JavaScript

花五分鐘把代碼註釋也規範一哈子?

Mondrian多維分析引擎數據緩存功能分析與擴展實現

一個合格的初級前端工程師需要掌握的模塊筆記