[JVM]類載入

Duancf發表於2024-07-12

類載入

載入

java資料型別分為基本資料型別和引用資料型別,
基本資料型別由虛擬機器預先定義,引用資料型別才需要類的載入過程。

類的載入,就是將java類的位元組碼檔案載入到記憶體中,並透過位元組碼在記憶體中構建出類的原型---類别範本物件。
jvm把位元組碼中的常量池,類欄位,類方法等資訊儲存到類别範本中,這樣jvm在執行期間才能獲得類的全部資訊。
反射也基於這一基礎。

載入階段主要做的事情:

首先透過類的全類名(包名加類名)找到類的位元組碼檔案,有下面幾種方法讀入位元組碼

  • 可以透過檔案系統讀入字尾為.class的檔案
  • 可以透過讀入jar,zip包,提取位元組碼
  • 可以讀取資料庫中的位元組碼
  • 可以使用http協議傳輸位元組碼
  • 可以使用執行時生成的位元組碼

解析位元組碼檔案,生成記憶體中的該類的資料結構,也就是類的模板,如果這個位元組碼不符合規範會丟擲classformaterror錯誤
類的模板存放在方法區中,方法區在元空間(使用本地記憶體)中

在堆中建立java.lang.class類的例項,指向上一步生成的類的模板,外部就透過堆中的這個例項來訪問類中的各種資訊。
java.lang.class的構造方法是私有的,只有jvm可以建立。

PS:陣列類的載入有些特殊,陣列類本身並不是由類載入器負責的,而是jvm在執行時根據需要直接建立的,但陣列的元素型別仍然需要類載入器建立,

連結

驗證:保證載入的位元組碼是規範的
格式檢查:格式檢查和位元組碼的載入一起執行,格式檢查透過後,類載入器才會把類的位元組碼載入到方法區中。
(格式檢查之外的檢查會在類的位元組碼載入到方法區之後執行)
(格式檢查舉例包括,魔數檢查,版本檢查等)
語義檢查
位元組碼驗證
符號引用驗證

準備:準備階段就是為類的靜態變數分配記憶體,並將其設定為預設值,並不是程式碼裡的預設值,而是jvm給這些變數的預設值。
如果靜態變數是static final修飾的基本資料型別,直接賦值常量,在準備階段顯式賦值,也就是賦予程式碼中的指定值。
而如果是static final修飾的String,使用字面量賦值,在準備階段顯式賦值,也就是賦予程式碼中的指定值。
注意這裡只是靜態變數,類的靜態變數和類别範本一起放在方法區,而類的例項變數則分配在堆中。

解析:將類,介面,欄位,方法的符號引用轉換為直接引用
符號引用(Symbolic Reference): 符號引用是一種用符號來表示引用目標的引用形式。在符號引用階段,引用的目標並沒有直接指定目標的記憶體地址,而是以符號的形式表示。符號引用可以是類名、欄位名、方法名等,它們是一種抽象的引用。

直接引用(Direct Reference): 直接引用是指可以直接定位目標的引用形式。與符號引用不同,直接引用包含了目標的直接記憶體地址或偏移量,可以直接定位到目標。
方法區中本來儲存的都是一些符號,比如我使用了某個類的某個方法,但是隻存了類和方法的名字,僅有這些資訊是沒有辦法執行的,還要把這些符號轉成真正的地址。

PS:當java程式碼中直接使用字串常量時,就會在常量池中生成constant_string,他表示字串常量,並會引用constant_utf8的常量項,在常量池中會維護字串常量池,儲存所有出現過的字串常量並且沒有重複項。

初始化

初始化階段,為類的靜態變數賦值和執行靜態程式碼塊的過程,
類的初始化階段才會執行java位元組碼,也就是java程式中的程式碼
類的初始化階段最重要的工作是執行類的初始化方法()
該方法由編譯器生成,jvm執行,程式設計師是無法呼叫的,也不能定義一個同名的方法。
這個方法的主要內容就是類的靜態變數的賦值語句和靜態程式碼塊
另外,在嘗試初始化一個類的時候,jvm總是會試圖先載入該類的父類,
因此父類的()函式總是在子類的()函式之前呼叫,也就是說父類的static靜態程式碼塊優先於子類的靜態程式碼塊。

下面有一些特殊情況,有些類的位元組碼檔案中不會產生()函式,
如果類中沒有靜態變數和靜態程式碼塊,自然就不會有()函式
如果類中有靜態變數但是沒有靜態變數的賦值語句,也沒有靜態程式碼塊,自然就不會有()函式

注意:以上可以看到有靜態變數的賦值就會有clinit函式,但是有一些特殊情況,即使有靜態變數的賦值也不會產生clinit函式,這是因為這些顯式賦值在連結的準備階段就做了。
如果靜態變數是static final修飾的基本資料型別,並且直接賦值常量,那麼在準備階段就已經被顯式賦值了,不會生成()函式
而如果是static final修飾的String,使用字面量賦值,那麼在準備階段就已經被顯式賦值了,不會生成()函式。

()方法需要執行緒安全,如果有多個執行緒想去初始化同一個類也就是執行同一個()方法,那麼應該只會有一個執行緒被允許去執行這個方法,其他執行緒都要被阻塞。
如果一個執行緒已經載入了類,那麼其他執行緒都不應該重複載入這個類,當需要再次使用這個類時,虛擬機器會直接返回已經準備好的資訊。

類的主動使用

只有類的主動使用會呼叫方法,
主動使用包括,

建立一個類的例項的時候,包括使用new關鍵字,使用反射,克隆,序列化
呼叫類的靜態方法的時候,
使用類或者介面的靜態欄位(注意有一些加final的常量在準備階段就已經產生了,所以不需要調方法),
使用java.lang.reflect包中的方法反射類的方法的時候,比如使用class.forname("")
當初始化子類發現父類還沒有進行過初始化,需要先觸發其父類的初始化
這條規則對介面並不適用,初始化一個類的時候,並不會初始化它所實現的介面
初始化一個介面的時候,並不會初始化父介面
如果一個介面定義了default方法,那麼當初始化直接或間接實現該介面的類時,會先觸發介面的初始化。
當虛擬機器啟動時,使用者需要指定一個要執行的主類,虛擬機器會先初始化這個主類
當初次呼叫methodhandle例項時,初始化該Methodhandle指向的方法所在的類。
為什麼需要自定義類載入器

首先介紹自定義類的應用場景:
(1)加密:Java程式碼可以輕易的被反編譯,如果你需要把自己的程式碼進行加密以防止反編譯,可以先將編譯後的程式碼用某種加密演算法加密,類加密後就不能再用Java的ClassLoader去載入類了,這時就需要自定義ClassLoader在載入類的時候先解密類,然後再載入。
(2)從非標準的來源載入程式碼:如果你的位元組碼是放在資料庫、甚至是在雲端,就可以自定義類載入器,從指定的來源載入類。
(3)以上兩種情況在實際中的綜合運用:比如你的應用需要透過網路來傳輸 Java 類的位元組碼,為了安全性,這些位元組碼經過了加密處理。這個時候你就需要自定義類載入器來從某個網路地址上讀取加密後的位元組程式碼,接著進行解密和驗證,最後定義出在Java虛擬機器中執行的類。

類的主動使用

關於在什麼情況下需要開始類載入過程的第一個階段“載入”,《Java虛擬機器規範》中並沒有進行強制約束,這點可以交給虛擬機器的具體實現來自由把握。但是對於初始化階段,《Java虛擬機器規範》則是嚴格規定了有且只有六種情況必須立即對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始):

  1. 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果型別沒有進行過初始化,則需要先觸發其初始化階段。能夠生成這四條指令的典型Java程式碼場景有:

    • 使用new關鍵字例項化物件的時候。
    • 讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候。
    • 呼叫一個型別的靜態方法的時候。
  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虛擬機器規範》中使用了一個非常強烈的限定語——“有且只有”,這六種場景中的行為稱為對一個型別進行主動引用。除此之外,所有引用型別的方式都不會觸發初始化,稱為被動引用。

相關文章