Java虛擬機之類加載機制

Java虛擬機之類加載機制

導語:在上一篇博客中我們對Java虛擬機的內存分配和回收進行了介紹和分析,本篇博客將主要針對Java虛擬機的類加載機制,包括類的生命週期、加載過程、類加載器以及雙親委派模型等做詳細的介紹

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制;類是在運行期間第一次使用時動態加載的,而不是一次性加載所有類,因爲如果一次性加載類,會佔用很多的內存


類的聲明週期

當程序主動使用某個類時,如果該類還未被加載到內存中,則JVM會通過加載、連接、初始化3個步驟來對該類進行初始化

在這裏插入圖片描述

類從被加載到虛擬機內存中開始,到卸載出內存爲止,整個生命週期包括:加載 -> 驗證 、準備 ->、解析 -> 初始化 -> 使用 -> 卸載7個階段,其中驗證、準備、解析3個部分統稱爲連接


類加載過程

加載過程主要包含了加載、驗證、準備、解析和初始化這 5 個階段,我們可以暫時先忽略使用以及卸載這兩個階段

加載指的是將類的class文件讀入到內存,併爲之創建一個java.lang.Class對象,也就是說,當程序中使用任何類時,系統都會爲之建立一個java.lang.Class對象,這裏要注意加載只是類加載的一個階段,加載並不是類加載

---->加載

  • 通過類的完全限定名稱獲取定義該類的二進制流
  • 將該字節流表示的靜態存儲結構轉化爲方法區的運行時結構
  • 在內存中生成一個代表該類的 Class 對象,作爲方法區中該類各種數據的訪問入口

二進制字節流可以從以下方式中獲取:

  • 從本地文件系統加載class文件
  • 從JAR、WAR、EAR等包加載class文件
  • 通過網絡加載class文件(典型例子applet)
  • 把一個Java源文件動態編譯(例如動態代理技術),並執行加載

---->連接(驗證、準備、解析)

當類被加載之後,系統爲之生成一個對應的Class對象,接着將會進入連接階段,連接階段負責把類的二進制數據合併到JRE中;連接又可分爲如下3驗證、準備、解析三個階段

驗證

驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其他類協調一致;確保了 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全,其主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證:

  • 文件格式驗證
    驗證字節流是否符合Class文件格式規範,並能被當前虛擬機所加載處理
  • 元數據驗證
    對字節碼描述的信息進行語義分析,分析是否符合Java語言規範
  • 字節碼驗證
    分析數據流和控制,確定語義的合法性、邏輯性;主要針對元數據驗證後對方法體的驗證,保證類的方法在運行時正常
  • 符號引用驗證
    針對符號引用轉換爲直接引用,確定訪問類型等涉及到引用的情況,保證引用一定會訪問到,避免出現類無法訪問的問題

準備

  • 類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存
  • 實例變量不會在這階段分配內存,它會在對象實例化時隨着對象一起被分配在堆中;實例化不是類加載的一個過程,類加載發生在所有實例化操作之前,並且類加載只進行一次,實例化可以進行多次

解析

將常量池的符號引用替換爲直接引用的過程,其中解析過程在某些情況下可以在初始化階段之後再開始,這是爲了支持 Java 的動態綁定

---->初始化

初始化是爲類的靜態變量賦予正確的初始值,準備階段和初始化階段看似有點矛盾,其實是不矛盾的,如下:

如果類中有語句:private static int a = 10,它的執行過程是首先字節碼文件被加載到內存後,先進行鏈接的驗證這一步驟,驗證通過後準備階段,給a分配內存,因爲變量a是static的,所以此時a等於int類型的默認初始值0,即a=0,然後到解析,之後到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10


類初始化時機

主動引用

虛擬機規範中並沒有強制約束何時進行加載,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(加載、驗證、準備都會隨之發生):

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

被動引用

以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:

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

     System.out.println(SubClass.value);  // value 字段在 SuperClass 中定義
    
  • 通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法

     SuperClass[] sca = new SuperClass[10];
    
  • 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

     System.out.println(ConstClass.HELLOWORLD);
    

類與類加載器

  • 兩個類相等,需要類本身相等,並且使用同一個類加載器進行加載。這是因爲每一個類加載器都擁有一個獨立的類名稱空間
  • 兩個類相等,需要類本身相等,並且使用同一個類加載器進行加載。這是因爲每一個類加載器都擁有一個獨立的類名稱空間

類加載器分類

  • 類加載器負責加載所有的類,其爲所有被載入內存中的類生成一個java.lang.Class實例對象,一旦一個類被加載如JVM中,同一個類就不會被再次載入了。正如一個對象有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識
  • 在Java中,一個類用其全限定類名(包括包名和類名)作爲標識;但在JVM中,一個類用其全限定類名和其類加載器作爲其唯一標識

從 Java 虛擬機的角度來講,只存在以下兩種不同的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader),使用 C++ 實現,是虛擬機自身的一部分
  • 所有其它類的加載器,使用 Java 實現,獨立於虛擬機,繼承自抽象類 java.lang.ClassLoader

從 Java 開發人員的角度看,類加載器可以劃分得更細緻一些:

  • 啓動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的類庫加載到虛擬機內存中。啓動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啓動類加載器,直接使用 null 代替即可
  • 擴展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的所有類庫加載到內存中,開發者可以直接使用擴展類加載器
  • 應用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器

雙親委派模型(類加載機制)

JVM類加載機制主要由如下三種:

  • 全盤負責:所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴和引用其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
  • 雙親委派:所謂的雙親委派,則是先讓父類加載器試圖加載該Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
  • 緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區中搜尋該Class,只有當緩存區中不存在該Class對象時,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩衝區中。修改了Class後,必須重新啓動JVM,程序所做的修改纔會生效

雙親委派模型

在這裏插入圖片描述
1.工作過程

一個類加載器首先將類加載請求轉發到父類加載器,只有當父類加載器無法完成時才嘗試自己加載

2.優點

  • 使得 Java 類隨着它的類加載器一起具有一種帶有優先級的層次關係,從而使得基礎類得到統一
  • 例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 並放到 ClassPath 中,程序可以編譯通過。由於雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先級更高,這是因爲 rt.jar 中的 Object 使用的是啓動類加載器,而 ClassPath 中的 Object 使用的是應用程序類加載器。rt.jar 中的 Object 優先級更高,那麼程序中所有的 Object 都是這個 Object

3.實現

以下是抽象類 java.lang.ClassLoader 的代碼片段,其中的 loadClass() 方法運行過程如下:先檢查類是否已經加載過,如果沒有則讓父類加載器去加載。當父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試自己去加載

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

結語:到此關於Java虛擬機的類加載機制就介紹到這,在此博客中之前也介紹了關於JVM的其他知識,如果想更多的瞭解JVM,請大家多多支持關注

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