當前位置:網站首頁>JVM詳解 --- 類加載機制

JVM詳解 --- 類加載機制

2022-01-27 16:09:31 程序員社區

JVM詳解 --- 類加載機制 , 你了解嗎? 本文就為大家帶來了一篇 JVM詳解 --- 類加載機制 一起看看吧!
另外小編還收集了很多不錯的編程資源,希望對你有幫助:點擊查看
祝您生活愉快~

JVM詳解 --- 類加載機制插圖
類加載機制思維導圖

1.類的生命周期

Java類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、准備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個階段。其中准備、驗證、解析3個部分統稱為連接(Linking),如下圖所示。

JVM詳解 --- 類加載機制插圖1
類的生命周期

注意:加載、驗證、准備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情况下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。

1.1類加載過程

當程序主動使用某個類時,如果該類還未被加載到內存中,則JVM會通過加載、連接、初始化3個步驟來對該類進行初始化。如果沒有意外,JVM將會連續完成3個步驟,所以有時也把這個3個步驟統稱為類加載或類初始化。

1.1.1加載

類的加載過程主要完成三件事:

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

值得注意的是,加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。加載是類加載的一個階段,注意不要混淆。

ps:描述一下JVM加載Class文件的原理機制

Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內存中。在寫程序的時候,我們幾乎不需要關心類的加載,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。

類裝載方式,有兩種 :

1.隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,
2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類

Java類的加載是動態的,它並不會一次性將所有類全部加載後再運行,而是保證程序運行的基礎類(像是基類)完全加載到jvm中,至於其他類,則在需要的時候才加載。這當然就是為了節省內存開銷。

1.1.2 連接

當類被加載之後,系統為之生成一個對應的Class對象,接著將會進入連接階段,連接階段負責把類的二進制數據合並到JRE中。類連接又可分為如下3個階段。

(1)驗證:驗證階段主要就是對文件格式,元數據,字節碼以及符號應用的一些驗證,個人感覺跟編譯檢查一樣的工作。確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。其主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

  • 文件格式驗證:主要驗證字節流是否符合Class文件格式規範,並且能被當前的虛擬機加載處理。例如:主,次版本號是否在當前虛擬機處理的範圍之內。常量池中是否有不被支持的常量類型。指向常量的中的索引值是否存在不存在的常量或不符合類型的常量。
  • 元數據驗證:對字節碼描述的信息進行語義的分析,分析是否符合java的語言語法的規範。
  • 字節碼驗證:最重要的驗證環節,分析數據流和控制,確定語義是合法的,符合邏輯的。主要的針對元數據驗證後對方法體的驗證。保證類方法在運行時不會有危害出現。
  • 符號引用驗證:主要是針對符號引用轉換為直接引用的時候,是會延伸到第三解析階段,主要去確定訪問類型等涉及到引用的情况,主要是要保證引用一定會被訪問到,不會出現類等無法訪問的問題。

(2)准備准備階段是為類的靜態變量分配內存並設置默認初始值,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:

  • 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。
  • 這裏所設置的初始值"通常情况"下是數據類型默認的零值(如0、0L、null、false等),比如我們定義了public static int value=111 ,那麼 value 變量在准備階段的初始值就是 0 而不是111(初始化階段才會賦值)。特殊情况:比如給 value 變量加上了 fianl 關鍵字public static final int value=111 ,那麼准備階段 value 的值就被賦值為 111。

(3)解析:將類的二進制數據(存儲在常量池)中的符號引用替換成直接引用。說明一下:符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何的字面形式的字面量,只要不會出現沖突能够定比特到就行。布局和內存無關。直接引用:是指向目標的指針,偏移量或者能够直接定比特的句柄。該引用是和內存中的布局有關的,並且一定加載進來的。

1.1.3 初始化

初始化是類加載的最後一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器方法的過程,即為類的靜態變量賦予正確的初始值。對於構造方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因為構造方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起死鎖,並且這種死鎖很難被發現。

ps:准備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存後,先進行鏈接的驗證這一步驟,驗證通過後准備階段,給a分配內存,因為變量a是static的,所以此時a等於int類型的默認初始值0,即a=0, 然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。

1.2 卸載

卸載類即該類的Class對象被GC。卸載類需要滿足3個要求:

  • 該類的所有的實例對象都已被GC,也就是說堆不存在該類的實例對象。
  • 該類沒有在其他任何地方被引用
  • 該類的類加載器的實例已被GC

所以,在JVM生命周期類,由jvm自帶的類加載器加載的類是不會被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的。

只要想通一點就好了,jdk自帶的BootstrapClassLoader,PlatformClassLoader,AppClassLoader負責加載jdk提供的類,所以它們(類加載器的實例)肯定不會被回收。而我們自定義的類加載器的實例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的。

2.類與類加載器

類加載器負責加載所有的類,其為所有被載入內存中的類生成一個java.lang.Class實例對象。一旦一個類被加載如JVM中,同一個類就不會被再次載入了。正如一個對象有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識。在Java中,一個類用其全限定類名(包括包名和類名)作為標識;但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標識。例如,如果在pg的包中有一個名為Person的類,被類加載器ClassLoader的實例kl負責加載,則該Person類對應的Class對象在JVM中錶示為(Person.pg.kl)。這意味著兩個類加載器加載的同名類:(Person.pg.kl)和(Person.pg.kl2)是不同的、它們所加載的類也是完全不同、互不兼容的。

ps:限定類名,就是類名全稱,帶包路徑的用點隔開,例如: java.lang.String,非限定類名是相對於限定類名來說的,在Java中有很多類,不同的類之間會存在相同的函數或者方法,所以有時候就需要限定類名來調包。 而如果不存在相同的函數或者方法 ,就可以使用非限定(non-qualified)類名。

2.1類加載器分類

JVM預定義有三種類加載器,當一個 JVM啟動的時候,Java開始使用如下三種類加載器:

  • 啟動類加載器/根加載器(Bootstrap ClassLoader):它用來加載 Java 的核心類,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader(負責加載$JAVA_HOME中jre/lib/rt.jar裏所有的class,由C++實現,不是ClassLoader子類)。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作
  • ExtensionClassLoader(擴展類加載器) :它負責加載JRE的擴展目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類。由Java語言實現,父類加載器為null。
  • AppClassLoader(應用程序/系統類加載器) :面向用戶的加載器,負責加載當前應用classpath下的所有jar包和類,一般來說,Java 應用的類都是由它來完成加載的。程序可以通過ClassLoader的靜態方法getSystemClassLoader()來獲取系統類加載器。如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作為父加載器。由Java語言實現,父類加載器為ExtClassLoader。
  • 用戶自定義類加載器:通過繼承 java.lang.ClassLoader類的方式實現。

2.2雙親委派模型

2.2.1 類加載機制

JVM的類加載機制主要有如下3種:

  • 全盤負責:所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴和引用其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。
  • 雙親委派:所謂的雙親委派,則是先讓父類加載器試圖加載該Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父加載器,依次遞歸,如果父加載器可以完成類加載任務,就成功返回;只有父加載器無法完成此加載任務時,才自己去加載。
  • 緩存機制。緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區中搜尋該Class,只有當緩存區中不存在該Class對象時,系統才會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩沖區中。這就是為很麼修改了Class後,必須重新啟動JVM,程序所做的修改才會生效的原因。

應用程序是由三種類加載器互相配合從而實現類加載,除此之外還可以加入自己定義的類加載器。

下圖展示了類加載器之間的層次關系,稱為雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類加載器外,其它的類加載器都要有自己的【父加載器】。這裏的父子關系一般通過組合關系(Composition)來實現,而不是繼承關系(Inheritance)。

JVM詳解 --- 類加載機制插圖2
雙親委派模型
2.2.2 雙親委派機制工作流程

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

總結:自底向上檢查類是否被加載過,自頂向下嘗試加載類。

(3)雙親委派模型優劣

優點:(1)保證安全(2)避免重複加載

  • 首先,使得Java類隨著它的類加載器一起具有一種帶有優先級的層次關系保證基礎類的安全與統一,即核心API不被篡改。假設通過網絡傳遞一個名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。
  • 其次,保證java程序更加穩定,可以避免類的重複加載,確保一個類的全局唯一性。(jvm區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類,兩者不兼容)

存在的問題:。。。。

如何打破雙親委派機制?

ps:打破雙親委派機制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法。

(4)具體實現

以下是抽象類 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);    }}

2.3 自定義類加載器

以下代碼中的 FileSystemClassLoader是自定義類加載器,繼承自 java.lang.ClassLoader,用於加載文件系統上的類。它首先根據類的全名在文件系統上查找類的字節代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass() 方法來把這些字節代碼轉換成java.lang.Class 類的實例。

java.lang.ClassLoader 的 loadClass()實現了雙親委派模型的邏輯,自定義類加載器一般不去重寫它,但是需要重寫 findClass()方法。

public class FileSystemClassLoader extends ClassLoader {    private String rootDir;    public FileSystemClassLoader(String rootDir) {        this.rootDir = rootDir;    }    protected Class<?> findClass(String name) throws ClassNotFoundException {        byte[] classData = getClassData(name);        if (classData == null) {            throw new ClassNotFoundException();        } else {            return defineClass(name, classData, 0, classData.length);        }    }    private byte[] getClassData(String className) {        String path = classNameToPath(className);        try {            InputStream ins = new FileInputStream(path);            ByteArrayOutputStream baos = new ByteArrayOutputStream();            int bufferSize = 4096;            byte[] buffer = new byte[bufferSize];            int bytesNumRead;            while ((bytesNumRead = ins.read(buffer)) != -1) {                baos.write(buffer, 0, bytesNumRead);            }            return baos.toByteArray();        } catch (IOException e) {            e.printStackTrace();        }        return null;    }    private String classNameToPath(String className) {        return rootDir + File.separatorChar                + className.replace('.', File.separatorChar) + ".class";    }}

3.類加載時機

主要包括類的創建(new或者反射),訪問(靜態變量或者靜態方法),初始化一個類的子類和標明啟動類。

  • 創建類的實例,也就是new一個對象
  • 反射(Class.forName("com.it.load"))
  • 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  • 調用類的靜態方法
  • 初始化一個類的子類(會首先初始化子類的父類)
  • JVM啟動時標明的啟動類,即文件名和類名相同的那個類

除此之外,下面幾種情形需要特別指出:

  • 對於一個final類型的靜態變量,如果該變量的值在編譯時就可以確定下來,那麼這個變量相當於“宏變量”。Java編譯器會在編譯時直接把這個變量出現的地方替換成它的值,因此即使程序使用該靜態變量,也不會導致該類的初始化。反之,如果final類型的靜態Field的值不能在編譯時確定下來,則必須等到運行時才可以確定該變量的值,如果通過該類來訪問它的靜態變量,則會導致該類被初始化。

拓展

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

1)當遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。

  • 當jvm執行new指令時會初始化類。即當程序創建一個類的實例對象。
  • 當jvm執行getstatic指令時會初始化類。即程序訪問類的靜態變量(不是靜態常量,常量會被加載到運行時常量池)。
  • 當jvm執行putstatic指令時會初始化類。即程序給類的靜態變量賦值。
  • 當jvm執行invokestatic指令時會初始化類。即程序調用類的靜態方法。

2)使用 java.lang.reflect 包的方法對類進行反射調用時如Class.forname("..."),newInstance()等等。 ,如果類沒初始化,需要觸發其初始化。

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

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

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

被動引用:除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。被動引用的常見例子包括:

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

System.out.println(SubClass.value);  // value 字段在 SuperClass 中定義

2)通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。

SuperClass[] sca = new SuperClass[10];

3)常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

System.out.println(ConstClass.HELLOWORLD);

6.參考

https://github.com/CyC2018/CS-Notes
https://github.com/Snailclimb/JavaGuide
https://blog.csdn.net/m0_38075425/article/details/81627349

版權聲明
本文為[程序員社區]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201271609305417.html

隨機推薦