簡述
類的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。
載入
①.通過一個類的全限定名來獲取定義此類的二進位制位元組流
②.將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
③.在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
虛擬機器設計團隊將第一步操作放置Java虛擬機器外部,以便讓應用程式自己決定去獲取所需要的類,實現這操作的程式碼模組稱為類載入器(可以通過繼承ClassLoader重寫loadClass()方法),JVM提供了3中類載入器:
主要載入核心類庫,負責存放在JAVA_HOMElib目錄中的,或者-Xbootclasspath引數指定路徑中的,並且被虛擬機器識別(僅按照檔名識別,如rt.jar)的類庫載入到虛擬機器記憶體中。控制檯輸出下面程式碼,可以看到具體哪些類庫。
System.getProperty("sun.boot.class.path")
複製程式碼
負責載入JAVA_HOMElibext目錄中,或者被java.ext.dirs系統變數指定路徑中的類庫。
負責載入使用者類路徑(ClassPath)上所指定的類庫
應用程式都是由這3種類載入器互相配合進行載入,我們自己也可以定義類載入器。JVM使用雙親委派模型來組織類載入器之間關係(組合關係)
雙親委派模型工作過程:如果一個類載入器收到類載入請求,它首先判斷這個class是不是已經載入成功,如果沒有委派父類載入器處理,逐層相同操作,最終載入請求都會傳遞到頂層的啟動類載入器,只有當父類載入器無法完成載入請求時,子載入器才會嘗試自己去載入。
雙親委派模型好處:因為不同類載入器載入同一個Class檔案會是兩個獨立的類,所以雙親委派模型讓Java類隨著它的類載入器一起具備一種優先順序的層次關係,例如類java.lang.Object,它存放在rt.jar包中,無論哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,確保了Object類在各種載入器環境中都是同一個類
驗證
為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全,主要完成下面4個階段:
驗證位元組流是否符合Class檔案格式規範,並且能被當前版本的虛擬機器處理,該驗證階段的主要目的是保證輸入的位元組流能正確地解析並儲存到方法區中。只有通過此驗證,位元組流才會進入記憶體的方法區進行儲存,後面3個階段都基於方法區的儲存結構進行,不直接操作位元組流。
對後設資料資訊中的資料型別校驗,以保證其描述的資訊符合Java語言規範的要求。e.g.是否繼承了final修飾的類等
對類的方法體進行校驗,保證類的方法執行時不會做出危害虛擬機器安全的事件。
對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,為了確保解析動作能正常執行。e.g.通過字串描述的全限定名是否能找到對應的類、是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位
準備
正式為類變數分配記憶體並在方法區設定類變數初始值
public static int value = 123;
複製程式碼
通常情況下變數value在準備階段過後var初始值為0而不是123,初始值為資料型別的預設值。到了初始化階段,value複製123會在類構造器clinit()方法執行。
public static final int value = 123;
複製程式碼
特殊情況在編譯階段會為value生成ConstantValue屬性,在準備階段虛擬機器會根據ConstantValue屬性將value賦值為100 (如果同時使用final和static來修飾一個變數,並且這個變數的資料型別是基本型別或者java.lang.String,就會生成ConstantValue屬性)。
解析
此階段虛擬機器將常量池內的符號引用替換為直接引用,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符。
符號引用:以一組符號來描述所引用的目標,可以是任何形式的字面量,符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中
直接引用:直接指向目標的指標、相對偏移量或一個能間接定位到目標的控制程式碼
初始化
到了初始化階段,類中的Java程式程式碼才開始執行,是執行類構造器clinit方法的過程,clinit方法由類變數賦值動作和靜態語句塊(static{})組成,順序由語句在原始檔出現的順序決定。
注:
①.clinit方法與init方法不同,它不需要顯式地呼叫父類clinit方法,虛擬機器會保證父類的clinit優先執行
②.clinit方法對於類或介面不是必須的,如果一個類中沒有靜態程式碼塊,也沒有靜態變數的賦值操作,那麼編譯器可以不為這個類生成clinit方法
③.行介面的clinit方法不需要先執行父介面的clinit方法,只有使用了父介面中定義的變數時,父介面才會初始化。介面的實現類在初始化時也一樣不會執行介面的clinit方法
④.如果多個執行緒同時去初始化一個類,只會有一個執行緒去執行這個類的clinit方法,其餘執行緒阻塞等待,當執行clinit方法的那條執行緒退出clinit方法後,其餘執行緒喚醒後也不會再進入clinit方法。同一個類載入器下,一個型別只會初始化一次
不同類載入器初始化:
public class ClassLoaderTest {
static {
System.out.println("clinit");
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
myLoader.loadClass("load.ClassLoaderTest").newInstance();
// Object obj = Class.forName("load.ClassLoaderTest").newInstance();
}
}
複製程式碼
輸出:
clinit
clinit
感謝
《深入理解Java虛擬機器》