Java面試之虛擬機類加載機制

參考:

《深入理解Java虛擬機》
https://thinkwon.blog.csdn.net/article/details/104390752
https://github.com/Snailclimb/JavaGuide

Java虛擬機類加載機制

1. 概述

    虛擬機把描述類的數據從Class文件文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
    與那些在編譯時需要進行連接工作的語言不同,在Java語言裏面,類型的加載、連接和初始化過程都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增加一些性能開銷,但是會爲Java應用程序提供高度的靈活性,Java裏天生可以動態擴展的語言特性就是依賴運行期動態加載和動態連接這個特點實現的。例如,如果編寫一個面向接口的應用程序,可以等到運行時再指定其實際的實現類;用戶可以通過Java預定義的和自定義類加載器,讓一個本地的應用程序可以在運行時從網絡或其他地方加載一個二進制流作爲程序代碼的一部分,這種組裝應用程序的方式目前已廣泛應用於Java程序之中。


2. 類加載的時機

在這裏插入圖片描述
    類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱爲連接(Linking),這7個階段的發生順序如上圖所示。
    上圖中,加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。
    什麼情況下需要開始類加載過程的第一個階段:加載?Java虛擬機規範中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。但是對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

  • 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類滅有進行過初始化,足額需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

這5種場景種的行爲稱爲對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用


3. 類加載的過程

3.1 加載

    “加載”是“類加載”(Class Loading)過程的一個階段。在加載階段,虛擬機需要完成以下3件事情:

  • 通過一個類的全限定名來獲取定義此類的二進制字節流
    沒有指明要從哪裏獲取、怎樣獲取。
  • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
  • 在內存種生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口
3.2 驗證

    驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流種包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。因此,驗證階段是非常重要的,這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊,從執行性能的角度上講,驗證階段的工作量在虛擬機的類加載子系統種又佔了相當大的一部分。驗證階段大致上會完成下面4個階段的檢驗動作:

  • 文件格式驗證
  • 元數據驗證
  • 字節碼驗證
  • 符號引用驗證
3.3 準備

    準備階段是正式爲類變量(被static修飾的變量)分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區種進行分配。這裏不包括實例變量。並且初始值“通常情況下”是數據類型的零值,假設一個類變量的定義如下:

public static int value = 123;

那變量value在準備階段過後的初始值爲0而不是123,因爲這時候尚未開始執行任何Java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器< clinit>()方法之中,所以把value賦值爲123的動作將在初始化階段纔會執行。
    上面提到,在通常情況下初始值是靈芝,那相對的會有一些“特殊情況”:如果類字段的字段屬性表種存在ConstantValue屬性,那再準備階段變量value就會被初始化爲ConstantValue屬性所指定的值,假設上面類變量value的定義變爲:

public static final int value = 123;

編譯時Java將會爲value生成ConstantValue屬性,再準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。

3.4 解析

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

  • 類或接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析
3. 5 初始化

    類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了再加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類種定義的Java程序代碼(或者說是字節碼)。


4. 類加載器

    虛擬機設計團隊把類加載階段種的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部區實現,以便讓應用程序自己決定如何區獲取所需要的類。實現這個動作的代碼模塊稱爲“類加載器”。
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其再Java虛擬機種的唯一性。每一個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱將 class 文件加載到 JVM 內存,然後再轉化爲 class 對象。

類加載器種類
  • BootStrapClassLoader : C++編寫,加載核心庫 java.*
  • ExtClassLoader : Java編寫,加載擴展庫 javax.*
  • AppClassLoader :Java編寫,加載程序所在目錄 ClassPath
  • 自定義ClassLoader:Java編寫,定製化加載,可以加載自定義目錄下的文件。
雙親委派模型

如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啓動類加載器中,只有當父加載無法完成加載請求(它的搜索範圍中沒找到所需的類)時,子加載器纔會嘗試去加載類。

loadClass 和 forName 的區別
  • loadClass獲得的Class對象只完成了加載過程,後面的連接、初始化並沒有完成。SpringIOC中的延遲加載就是用loadClass來完成。
  • forName獲得的Class對象完成了初始化過程。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章