jvm初探(一):java類加載機制與過程

jvm初探(一):java類加載機制與過程


我們知道,在我們編寫一個java文件,到java文件的運行,這一過程中,java文件經歷了從 .java到.class到本地java虛擬機解釋運行。class文件中的各種信息,最終都要加載到虛擬機中才能運行和使用。而虛擬機如何加載這些Class文件呢?Class文件中的信息進入到虛擬機後會發生什麼變化呢?這就是本篇文章要講的內容。

類加載機制:虛擬機將描述類的數據衝Class文件加載到內存,並對數據進行校驗、轉化、和初始化,最終形成可以被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。

在正式開始介紹類加載機制之前,我們需要知道一點:Class文件並不僅僅指java編譯器編譯而成的字節碼文件,它還有可能是:

  1. 數據庫中存儲的字節碼信息
  2. 從網絡中讀取,如Applet
  3. 從ZIP包讀取
  4. 其它二進制字節流等其他形式

一.類加載的時機:

類從加載到虛擬機內存中開始,到卸載出內存爲止,它的生命週期包括:
加載-----連接(驗證-----準備-----解析)-----初始化-----使用-----卸載

其中,加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序開始(是開始,而不是結束,通常都是開始之後交叉進行以及結束),而解析階段則不一定,這是爲了支持java的運行時綁定(也稱動態綁定)。
——————————————————————————————————————————————————————————
那什麼情況開始類加載的第一個階段:加載?Java虛擬機規範中並沒有進行強制約束。但是對於初始化階段,虛擬機規範嚴格規定了有且只有5個情況必須立即對類進行“初始化”(加載、驗證、準備)

  1. 遇到new、getstatic、putstatic、invokestatic這4條字節碼指令時,如果沒有對類進行過初始化,則需要先觸發其進行初始化,通常對應着:new一個對象、獲取或設置一個靜態字段、調用一個靜態方法
  2. 使用反射技術對類進行反射調用的時候
  3. 當初始化一個類的時候,如果其父類還沒初始化,那就觸發其父類進行初始化
  4. 當虛擬機啓動時,如果用戶需要指定一個要執行的主類(有main()方法的那個類),虛擬機會先初始化這個類
  5. 當使用jdk1.7以上的動態語言時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getsStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化————————————————————————————————————————————————————————————
    這5中場景的行爲都稱爲對一個類進行主動引用。除此之外的引用類的方式都不會觸發其初始化,稱爲被動引用,例如:

1 . 通過子類引用父類的靜態字段,不會導致子類初始化

class Super{
	static {
		System.out.println("super init");
}
	public static int v = 123;
}

class Subclass extends Super{
	static{
		System.out.print("Sub init");
    }
}

public class Test{
	public static void main(String[] args){
		System.out.println("Subclass.v");
	}
}

上述代碼只會輸出Super init與123,而不會輸出Sub init。對於靜態字段,只有直接定義該字段的類纔會被初始化,因此通過子類來引用父類中定義的靜態字段,只會觸發父類初始化。

  1. 通過數組定義來引用類,不會觸發類的初始化:
	Test[] arr = new Test[10];
  1. 常量在編譯階段會存入調用類的常量池中,本質上並沒用直接引用到定義常量的類,因此並不會觸發定義類的初始化

額外的,對於接口來說,並不會要求其父接口都完成了初始化,只有在真正使用到父接口的時候(比如引用接口中定義的常量)纔會初始化

二、類加載的過程

  1. 加載
    加載是類加載的第一步,這個過程中,主要完成3個事情:
    ①通過一個類的全限定名來獲取定義此類的二進制字節流
    ②將這個二進制字節流中的靜態存儲結構轉化成虛擬機中方法區的運行時數據 結構
    ③在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的 數據訪問入口

需要注意的是:數組類並不通過類加載器創建,它是由java虛擬機直接創建的

  1. 驗證
    驗證是連接階段的第一步,這一階段的目的就是爲了確保class文件的安全性與合法性,確保不會有危害到虛擬機的安全,主要包括:
    文件格式驗證:主要驗證字節流是否符合class文件的規範,並且能被當前虛擬機處理
    元數據驗證:主要對字節碼描述的信息進行語義分析,以保證其描述的信息返回java語言的規範要求,如是否有父類、是否繼承了不允許繼承的類、是否實現了所有抽象方法
    字節碼驗證:主要目的是通過數據流和控制流分析,確定程序語義是合法,符合邏輯的。
    符號引用驗證:這個過程發生在解析階段的進行中,主要是對類自身以外的信息(如常量池中的各種符號引用)進行匹配性校驗

  2. 準備:準備階段是正式爲類變量分配內存並設置類變量初始值的階段。這裏的設置初始值通常是設置爲0值或null等,而不是我們自定義的初始值,我們自定義的初始值通常是在初始化階段來設置上去的。
    **需要注意的是:**如果類字段的字段屬性表存在ConstantValue屬性,那在準備階段就會初始化爲指定的值,如:public static final int v =123;

  3. 解析:該階段主要將常量池中的符號引用替換爲直接引用

  4. 初始化: 類初始化階段是類加載過程的最後一步,該階段主要任務是按照程序員自定義的計劃去初始化類變量與其他資源,初始化階段是執行類構造器< clint>()方法的過程,下面我們介紹一下< clint>()方法
    ①< clint>()方法是有編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併而成的,收集的順序是由語句在源文件中出現的順序所決定的
    ②虛擬機會保證在子類的< clint>()執行之前,父類的< clint>()已經執行完畢。
    ③由於第②點,父類中定義的靜態語句塊必定優先於子類的靜態語句塊
    ④對於接口而言,只有當父接口中定義的變量使用時,父接口才會初始化。

————————————————————————————————————————————————————————————

三、類加載器:

在類加載的第一階段的第一件事情,我們是這麼描述的:通過一個類的全限定名來獲取定義此類的二進制字節流。jvm的設計者將這個動作放到了jvm外部去實現,以便讓應用程序自己決定如何去獲取所需要的類,實現這個動作的代碼模塊稱爲“類加載器”

我們不去討論加載的實現過程,我們來看看類加載器獨特的作用:
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確認其在jvm中的唯一性,每一個類加載器都擁有一個獨立的類名稱空間。例如,對於來自同一class文件中的同一個類,只要加載它們的類加載器不同,那這2個類就必定不相等。

雙親委派機制: 如果一個類加載器收到了類加載的請求,它首先不會自己去加載該類,而是把這個請求委派給父類加載器去完成,只有當父加載器反饋無法自己完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。

絕大部分類都會用到以下三種系統提供的類加載器:

  1. 啓動類加載器(Bootstrap ClassLoader):負責加載javahome/lib目錄中的類(按文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄也不會加載)
  2. 擴展類加載器(Extension ClassLoader):負責加載javahome/lib/ext目錄中的
  3. 應用程序類加載器(Application ClassLoader):加載用戶類路徑上所指定的類庫

程序員也可以自己定義類加載器,來完成自定義的目的。

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