JVM虛擬機 類加載過程與類加載器


前言

類裝載器子系統是JVM中非常重要的部分,是學習JVM繞不開的一關。

一般來說,Java 類的虛擬機使用 Java 方式如下:

  • Java 源程序(.java 文件)在經過 Java 編譯器編譯之后就被轉換成 Java 字節代碼(.class 文件)。
  • 類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class類的一個實例。
  • 每個這樣的實例用來表示一個 Java 類。
  • 通過此實例的 newInstance()方法就可以創建出該類的一個對象。

類的生命周期

我們先來看下類的生命周期,包括:

  • 加載
  • 連接
  • 初始化
  • 使用
  • 卸載

其中加載連接初始化屬於類加載過程

使用是指我們new對象進行使用。

卸載指對象被GC垃圾回收掉。

image-20210517215428824

類加載過程

JVM的類加載的過程是通過引導類加載器(bootstrap class loader)創建一個初始類(initial class)來完成的,這個類是由JVM的具體實現指定的。

Class 文件需要加載到虛擬機中之后才能運行和使用,系統加載 Class 類型的文件份如下幾步:

  • 加載
  • 連接
    • 驗證
    • 准備
    • 解析
  • 初始

順序是這樣一個順序,但是加載階段和連接階段部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。

下面我們來逐步解析

加載

這里的加載微觀上的,是類加載過程中的一小步,也是第一步,類加載過程中的加載宏觀上的。

加載的流程如下:

  • 通過全類名獲取定義此類的二進制字節流
  • 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
  • 在內存中生成一個代表該類的 Class 對象,作為方法區這些數據的訪問入口

簡單來說就是:加載二進制數據到內存 —> 映射成JVM能識別的結構—> 在內存中生成class文件

虛擬機規范上,對這部分的規定並不具體,所以實現方式是很靈活的。

加載階段我們可以用自定義類加載器去控制字節流的獲取方式,是非數組類的可控性最強的階段,而數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。

關於類加載器是什么,后文再聊。

連接

連接分為三步,驗證、准備、解析,目的是將上面創建好的Class類合並至JVM中,使之能夠執行的過程。

驗證

確保class文件中的字節流包含的信息,符合當前虛擬機的要求,保證這個被加載的class類的正確性,不會危害到虛擬機的安全。

准備

為類中的靜態字段分配內存,並設置默認的初始值,比如int類型初始值是0。

被final修飾的static字段不會設置,因為final在編譯的時候就分配了。

解析

解析階段的目的,是將常量池內的符號引用轉換為直接引用的過程。

解析動作主要針對類、接口、字段、類方法、接口方法、方法類型等。

如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那么解析將觸發這個類的加載(但未必觸發這個類的鏈接以及初始化。)

符號引用就是一組符號來描述目標,可以是任何字面量,符號引用的字面量形式明確定在《Java 虛擬機規范》的Class文件格式中。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

舉個例子:

在程序執行方法時,系統需要明確知道這個方法所在的位置

Java 虛擬機為每個類都准備了一張方法表來存放類中所有的方法

當需要調用一個類的方法的時候,只要知道這個方法在方法表中的偏移量就可以直接調用該方法了。

通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。

所以,解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量

初始化

初始化就是執行類的構造器方法,是類加載的最后一步,這一步 JVM才開始真正執行類中定義的 Java 程序代碼

這個方法不需要定義,是javac編譯器自動收集類中所有類變量的賦值動作靜態代碼塊中的語句合並來的。

若該類具有父類,jvm會保證父類的init()先執行,然后在執行子類的init()

對於初始化階段,虛擬機嚴格規范了有且只有 5 種情況下,必須對類進行初始化,只有主動去使用類才會初始化類:

  • 當遇到 newgetstaticputstaticinvokestatic 這 4 條直接碼指令時

    • 當遇到一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。
    • 當 JVM執行 new 指令時會初始化類。即當程序創建一個類的實例對象
    • 當 JVM執行 getstatic 指令時會初始化類。即程序訪問類的靜態變量(不是靜態常量,常量會被加載到運行時常量池)。
    • 當 JVM執行 putstatic 指令時會初始化類。即程序給類的靜態變量賦值
    • 當 JVM執行 invokestatic 指令時會初始化類。即程序調用類的靜態方法
  • 對類進行反射調用時,如果類沒初始化,需要觸發其初始化。

  • 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化

  • 當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。

  • MethodHandleVarHandle 可以看作是輕量級的反射調用機制,而要想使用這 2 個調用, 就必須先使用 findStaticVarHandle 來初始化要調用的類。

  • 「補充,來自issue745 當一個接口中定義了 JDK8 新加入的默認方法(被 default 關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。

類加載器

三大類加載器

了解了類加載過程后,我們來看看類加載器

類加載器(ClassLoader)用來加載 Java 類到 Java 虛擬機中。

JVM 中內置了三個重要的 ClassLoader,同時按如下順序進行加載:

  1. BootstrapClassLoader 啟動類加載器:最頂層的加載類,由C++實現,負責加載 %JAVA_HOME%/lib目錄下的核心jar包和類或者或被 -Xbootclasspath參數指定的路徑中的所有類。
  2. ExtensionClassLoader 擴展類加載器:主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。
  3. AppClassLoader 應用程序類加載器:面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。

除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader

類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器。

需要注意的是,Java虛擬機對Class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的Class文件加載到內存生成Class對象。

雙親委派模型

概念

每一個類都有一個對應它的類加載器。在加載類的時候,是采用的雙親委派模型,即把請優求先交給父類處理的一種任務委派模式。

系統中的類加載器在協同工作的時候會默認使用 雙親委派模型

雙親委派模型的理論很簡單,分為如下幾步:

  • 即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。

  • 加載的時候,首先會把該請求委派給該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啟動類加載器 BootstrapClassLoader 中。

  • 當父類加載器無法處理時,才由自己來處理

AppClassLoader的父類加載器為ExtensionClassLoader ExtensionClassLoader 的父類加載器為null,當父類加載器為null時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器。

為什么要使用雙親委派模型

試想一種情況,我們在項目目錄下,手動創建了一個java.lang 包,並在該包下創建了一個Object,這時候我們再去啟動Java程序,原生Object會被篡改嗎?當然是不會的!

因為Object類是Java的核心庫類,由BootstrapClassLoader加載,而自定義的java.lang.Object類應該是由AppClassLoader來加載。

BootstrapClassLoader先於AppClassLoader進行加載,根據上面的雙親委派模型的概念,我們可以知道,java.lang.Object類已經被加載,並且AppClassLoader要加載類之前都要先給其父類過目,所以自己寫的野類是無法撼動核心庫類的。

結論

雙親委派模型保證了Java程序的穩定運行,可以避免類的重復加載,也保證了 Java 的核心 API 不被篡改。

源碼分析

雙親委派模型的都集中在 java.lang.ClassLoaderloadClass() 中,相關代碼如下所示:

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,檢查請求的類是否已經被加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //父加載器不為空,調用父加載器loadClass()方法處理
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //父加載器為空,使用啟動類加載器 BootstrapClassLoader 加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //拋出異常說明父類加載器無法完成加載請求
                }
                
                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己嘗試加載
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

反雙親委派模型

雙親委派模型是Java默認的,假如我們不想用雙親委派,我們要怎么辦呢?

我們可以自定義一個類加載器,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,很明顯需要繼承 ClassLoader

從上面的源碼我們知道,雙親委派模型的都集中在 java.lang.ClassLoaderloadClass() 中,如果想打破雙親委派模型則需要重寫 loadClass() 方法。

如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。

參考

  • 《深入理解Java虛擬機》第三版,吹爆!吹爆!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM