JVM - 類加載機制

類加載子系統在 JVM 中的位置

首先我們來從宏觀的角度看看,類加載機制在整個Java虛擬機中處於一個什麼位置,先來一張JVM的組成結構圖:
JVM 結構圖
     可以看到類加載子系統是屬於Java虛擬機的上層建築,只有將類從class二進制流加載到內存中,並校驗準備解析通過才能正式的被Java 虛擬機所使用,之後纔會討論到運行時數據區,執行引擎。
     虛擬機把類的數據從class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。 這裏說的類的數據,本質上是符合class文件要求的二進制字節流,並不一定要求必須是存放在磁盤上的.class 文件,任何形式都可以,可以從網絡傳輸獲取,甚至可以從數據庫獲取

類加載的時機

    一個類從被加載到內存開始,到卸載出內存爲止,稱爲類的生命週期,這包含 7 個步驟。
類生命週期
     加載、驗證、準備、初始化和卸載這5個階段的開始順序是確定的,類的加載過程必須按照這個順序按部就班的開始,但是解析階段卻不一定,它可能在初始化之前開始,也有可能在初始化之後才進行,這是爲了支持Java的運行時綁定。
     注意一下,這裏說的是按部就班的“開始”,並不是按部就班的“進行”或“結束”,也就是說這幾個階段的開始順序是確定的,但是執行過程有可能是交叉進行的,經常是一邊加載一邊驗證,加載過程尚未結束的時候,驗證過程已經開始。

初始化時機:主動引用與被動引用

那麼什麼時候會開始一個類加載過程的第一個階段:加載? 虛擬機規範並沒有進行強制性的規定,具體的策略依賴於具體的Java虛擬機實現。但虛擬機規範對類的初始化階段做了嚴格的限定:有且只有以下5種情況下,纔會對類進行初始化(當然加載、驗證、準備過程要在這之前已經開始)

  1. 遇到 new, getstatic, putstatic, invokestatic 指令碼時,如果一個類還沒被初始化過,必須進行類初始化。在 java 語言中,生成這幾條指令最常見的場景是:new 一個對象,讀取類的靜態變量,設置類的靜態變量(被static finall 修飾的類變量除外,這些變量在編譯期已經實現放入常量池中)、調用類的靜態方法的時候。
  2. 通過java.lang.reflect 包中的api,對類進行反射調用的時候,如果類還沒有初始化過,則觸發初始化過程
  3. 初始化一個類的時候,會先初始化其父類,如果父類還沒有被初始化過,則先觸發父類的初始化過程。(初始化接口並不需要先初始化其父接口)
  4. java虛擬機啓動的時候,會指定一個類的main方法作爲入口,需要觸發該類的初始化過程
  5. 當使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果是REF_getStatic, REF_putStatic,REF_invokeStatic 的方法句柄,如果這個方法所屬的類還沒有初始化過,觸發該類的初始化。

    java虛擬機規範對上面的5種情況用了一個”有且僅有“ 的限定詞,有就是說有且僅有遇到上面的5種情況時,纔會觸發類的初始化過程,其他所有不屬於這5中情況的都不會觸發類的初始化。基於此,我們把這5種情況稱之爲類的主動引用,除此之外,所有的其餘的引用類的方式稱爲類的被動引用。
     乍一聽,除了這5種,還有別的方式引用類?咋一時想不想起來呢 ?? 其實是有的。主要有一下幾種方式: 通過數組定義引用類(通過集合類引用某個類)、通過子類引用父類的靜態變量,常量在編譯階段會存入調用類的常量池中,本質上不會觸發定義常量的類的初始化。

被動引用舉例

類加載過程

裝載

裝載過程主要由3個基本動作組成,要裝載一個類型,Java虛擬機必須:

  1. 通過全限定名找到代表該類型的二進制數據流
  2. 解析這個二進制數據流爲方法區內的內部數據結構
  3. 創建一個表示該類型的 java.lang.Class類的實例
    找到一個類型的 class二進制數據流之後,Java虛擬機必須對這個數據進行足夠的處理,最後才能創建一個 java.lang.Class 類的實例來代表這個類型。虛擬機必須把這個數據流解析爲與具體實現相關的內部數據結構,裝載步驟的最終產品就是這個 java.lang.Class 類的實例, 它成爲應用程序與內部數據結構之間的接口。要訪問類型的信息,就通過這個實例來訪問(類的信息存儲在內部數據結構上,而不同的虛擬機實現有不同的表示類信息的內部數據結構),這樣對應用程序來說,就屏蔽了不同虛擬機實現對類信息的存儲細節。

驗證

在裝載開始之後,就準備進行連接了。連接的第一步就是驗證: 確認類型符合 Java 語言的語義,並且不會危害 Java 虛擬機的安全。

準備

準備階段爲類變量分配內存,並設置默認初始值。這裏的初始值是指類型的初始值,並不是初始化時應用程序賦予的真正的初始值。(準備階段不執行Java代碼),類型的初始值是什麼呢,
在這裏插入圖片描述
引用類型的初始值是 null

解析

解析階段就是將類信息(準確的說是類型的常量池)中的符號引用替換成直接引用的過程。對於符號引用,各種文章講了很多,我也有點糊塗。我這裏說一下我自己的理解: 例如有這麼一段java代碼:

UserService service = new UserService();
service.getUserList();

這段代碼編譯成class文件,那麼在class文件中肯定有一串字符來表示UserService 這個類(一一般來就是類的權限定名),
有一串字符來表示getUserList()方法,這些字面上的符號就是符號引用,那麼在運行時要真正的用到UserService類,前提是UserService類已經被加載到內存,
那麼就需要將這個字符替換爲UserService類在內存中的引用,這個引用可以是UserService類在內存中的指針,也可能是內存地址偏移量,也有可能是一個句柄,無論它是什麼形式,
它必須能夠直接或間接地定位到UserService類在內存中的位置,這樣才能去使用它。對方法也是如此,編譯時產生的代表getUserList方法的字符也要轉換爲這個方法在內存中的位置,否則無法執行。

初始化

連接步驟之後,就可以進入到初始化階段了。初始化階段給類變量賦予應用程序想要的正確的初始值,以及在類被正式使用前做一些準備工作。通俗點講,這個階段執行static靜態代碼塊以及類變量賦初始化值。

public class HelloWorld{
static String name = "helloworld"   // 類變量初始化語句
static {    // 靜態初始化語句
	System.out.println("initialization success")	
}
}

     在準備階段, name的值是null,只有在初始化階段,纔會賦予它正確的“初始值”。
在編譯時,Java編譯器將類的所有類變量初始化語句和靜態初始化語句都放入到一個 方法中,順序就按照他們在源代碼中的順序一致。這個 方法又稱爲類的初始化方法,它並不等同於類構造器方法。Java虛擬機保證在執行類的 方法之前,會先確保其父類的 方法已經執行過,不像構造器方法一樣,你需要顯示地調用其父類構造器,也就是說一個類在初始化之前,必定會先初始化其父類,其父類的初始化過程同樣如此,所以Object類肯定是最先被初始化的 。 方法並不是所有類都會存在,只有在必要的情況下它纔會出現,什麼是必要的情況呢,也就是說它能不出現就不出現。如果一個類沒有任何的類靜態變量(或者這個類靜態變量不需要初始化)也沒有任何的靜態初始化語句,那麼類就不會有 方法。但是即使一個類沒有 方法,也並不代表它的父類沒有 方法,在初始化時,它的父類也是要先初始化的。而且這個方法 是由java虛擬機調用的,我們的java程序是無法調用它的。
     說這麼多,可能有人會有個疑問:既然我們無法顯示調用它,要理解那麼多幹嘛?對我們的寫代碼能力沒有任何幫助嘛! 那就來說點乾貨,對我們的編碼也能夠起到一定的指導意義: Java虛擬機需要保證一個類只初始化一次,那麼也就是說在多線程環境下,它必須被正確的同步,如果多個線程同時對一個類進行初始化,只能有一個線程能夠執行初始化,其他線程會等待!也就是說,我們最好在類初始化中不要做太繁重的工作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章