JVM 第三篇:Java 類加載機制

2021-03-02 極客挖掘機

本文內容過於硬核,建議有 Java 相關經驗人士閱讀。

❞1. 什麼是類的加載?

類的加載指的是將類的 .class 文件中的二進位數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class 對象, Class 對象封裝了類在方法區內的數據結構,並且向 Java 程式設計師提供了訪問方法區內的數據結構的接口。

類加載器並不需要等到某個類被 「首次主動使用」 時再加載它, JVM 規範允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了 .class 文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。

加載.class文件的方式
– 從本地系統中直接加載
– 通過網絡下載.class文件
– 從zip,jar等歸檔文件中加載.class文件
– 從專有資料庫中提取.class文件
– 將Java源文件動態編譯為.class文件

2. 類的生命周期

一個類型從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱為連接(Linking)。

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

2.1 加載(Loading)

加載時類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:

將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口。

相對於類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進位字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。

加載階段完成後,虛擬機外部的 二進位字節流就按照虛擬機所需的格式存儲在方法區之中,而且在 Java 堆中也創建一個 java.lang.Class 類的對象,這樣便可以通過該對象訪問方法區中的這些數據。

2.2 驗證(Verification)

驗證是連接階段的第一步,這一階段的目的是確保 Class 文件的字節流中包含的信息符合「Java虛擬機規範」的全部約束要求,保證這些信息被當作代碼運行後不會危害虛擬機自身的安全。

驗證是連接階段的第一步,這一階段的目的是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:

文件格式驗證: 驗證字節流是否符合 Class 文件格式的規範;例如:是否以 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。元數據驗證:對字節碼描述的信息進行語義分析(注意:對比 javac 編譯階段的語義分析),以保證其描述的信息符合 Java 語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object 之外。字節碼驗證: 通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用 -Xverifynone 參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

2.3 準備(Preparation)

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:

這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。這裡所設置的初始值通常情況下是數據類型默認的零值(如 0 、 0L 、 null 、 false 等),而不是被在 Java 代碼中被顯式地賦予的值。2.4 初始化(Initialization)

類的初始化階段是類加載過程的最後一個步驟,之前介紹的幾個類加載的動作裡,除了在加載階段用戶應用程式可以通過自定義類加載器的方式局部參與外,其餘動作都完全由 Java 虛擬機來主導控制。直到初始化階段, Java 虛擬機才真正開始執行類中編寫的 Java 程序代碼,將主導權移交給應用程式。

在 Java 中對類變量進行初始值設定有兩種方式:

3. 類加載器

類加載器就是負責加載所有的類,將其載入內存中,生成一個 java.lang.Class 實例。一旦一個類被加載到 JVM 中之後,就不會再次載入了。

啟動類加載器(Bootstrap ClassLoader):其負責加載 Java 的核心類,比如 String 、 System 這些類。拓展類加載器(Extension ClassLoader):其負責加載 JRE 的拓展類庫。系統類加載器(System ClassLoader):其負責加載 CLASSPATH 環境變量所指定的 JAR 包和類路徑。用戶類加載器:用戶自定義的加載器,以類加載器為父類。

一個簡單的小慄子:

public static void main(String[] args) {
    ClassLoader loader = ClassLoader.getSystemClassLoader();
    System.out.println(loader);
    System.out.println(loader.getParent());
    System.out.println(loader.getParent().getParent());
}

輸出結果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

「為什麼根類加載器為 NULL ?」

啟動類加載器(Bootstrap Loader)並不是 Java 實現的,而是使用 C 語言實現的,找不到一個確定的返回父 Loader 的方式,於是就返回 null 。

「JVM 類加載機制」

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

雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。

雙親委派機制:

當 AppClassLoader 加載一個 class 時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器 ExtClassLoader 去完成。當 ExtClassLoader 加載一個 class 時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。如果 BootStrapClassLoader 加載失敗(例如在 $JAVA_HOME/jre/lib 裡未查找到該 class ),會使用 ExtClassLoader 來嘗試加載。若 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,如果 AppClassLoader 也加載失敗,則會報出異常 ClassNotFoundException 。

以下為 ClassLoader#loadClass 的源碼, JDK 版本為 1.8.0_221 。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先判斷該類型是否已經被加載
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 如果沒有被加載,就委託給父類加載或者委派給啟動類加載器加載
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 如果存在父類加載器,就委派給父類加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    // 如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法 native Class findBootstrapClass(String name)
                    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.
                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;
    }
}

雙親委派模型是為了防止內存中出現多份同樣的字節碼,保證程序穩定的運行。

5. 自定義類加載器

在最開始,我想先介紹下自定義類加載器的適用場景:

加密:Java 代碼可以輕易的被反編譯,如果需要把代碼進行加密以防止反編譯,可以先將編譯後的代碼用某種加密算法加密,這樣加密後的類就不能再用 Java 的 ClassLoader 去加載類了,這時就需要自定義 ClassLoader 在加載類的時候先解密類,然後再加載。從非標準的來源加載代碼:如果我們的字節碼是放在資料庫、甚至是在雲端,就可以自定義類加載器,從指定的來源加載類。

一個小案例,首先我們創建一個需要加載的目標類:

public class ClassLoaderTest {
    public void hello() {
        System.out.println("我是由 " + getClass().getClassLoader().getClass() + " 加載的");
    }
}

這個類先進行編譯,編譯後的 class 我放到了 D 盤的根目錄,然後刪除原本在項目中的 class 文件,如果不刪除的話,通過前面的雙親委派模型,我們會知道這個 class 會被 sun.misc.Launcher$AppClassLoader 進行加載。

然後我們定義一個自己的加載類:

public class MyClassLoader extends ClassLoader {
    public MyClassLoader(){}

    public MyClassLoader(ClassLoader parent){
        super(parent);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File("D:\\ClassLoaderTest.class");
        try{
            byte[] bytes = getClassBytes(file);
            //defineClass方法可以把二進位流字節組成的文件轉換為一個java.lang.Class
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private byte[] getClassBytes(File file) throws Exception {
        // 這裡要讀入.class的字節,因此要使用字節流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true){
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader classLoader = new MyClassLoader();
        Class clazz = classLoader.loadClass("com.geekdigging.lesson03.classloader.ClassLoaderTest");
        Object obj = clazz.newInstance();
        Method helloMethod = clazz.getDeclaredMethod("hello", null);
        helloMethod.invoke(obj, null);
    }
}

最後列印結果:

我是由 class com.geekdigging.lesson03.classloader.MyClassLoader 加載的

參考

https://www.cnblogs.com/ityouknow/p/5603287.html

https://www.cnblogs.com/twoheads/p/10143038.html

相關焦點

  • 你有真正理解 Java 的類加載機制嗎?|原力計劃
    作者 | 宜春責編 | Elle出品 | CSDN 博客你是否真的理解Java的類加載機制?點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了Java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解Java的類加載機制了。
  • 乾貨 | 高級Java工程師面試必備jvm類加載機制
    回復「新人」領取關注福利這篇文章不聊別的,專門來侃侃JVM的類加載機制。
  • 【143期】Java 類是如何被加載的?
    不過貿然的向別人解釋雙親委派模型是不妥的,如果在不了解JVM的類加載機制的情況下,又如何能很好的理解「不同ClassLoader加載的類是互相隔離的」這句話呢?所以為了理解雙親委派,最好的方式,就是先了解下ClassLoader的加載流程。二:Java 類是如何被加載的2.1:何時加載類我們首先要清楚的是,Java類何時會被加載?
  • 別翻了,這篇文章絕對讓你深刻理解java類的加載以及ClassLoader源碼分析
    點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解java的類加載機制了。類的加載問題,如果你對Java加載機制不理解,那麼你可能就錯了上面兩道題目的。
  • JVM 面試基礎準備篇(一)
    JVM 面試基礎準備篇(一)1. 計算機原理2.2.1.3.3 結構劃分2.2 類文件到虛擬機所謂類加載機制就是轉換解析和初始化形成可以虛擬機直接使用的Java 類型,即 java.lang.Class
  • 你知道java反射機制中class.forName和classloader的區別嗎?
    前兩天頭條有朋友留言說使用class.forName找不到類,可以使用classloader加載。趁此機會總結一下,正好看到面試中還經常問到。一、類加載機制上面兩種加載類的方式說到底還是為了加載一個java類,因此需要先對類加載的過程進行一個簡單的了解。
  • 深入理解Java虛擬機:類加載機制
    虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成可以被虛擬機直接使用的Java類型。一、類的生命周期二、類加載時機必須對類進行"初始化"的情況:使用new關鍵字實例化對象的時候讀取或設置一個類型的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候。
  • java創建對象的過程詳解(從內存角度分析)
    java對象的創建操作其實我在《JVM系列之類的加載機制》一文曾經提到過,包含兩個過程:類的初始化和實例化。為此為了理解的深入,我們還需要再來看一下類的生命周期。一張圖表示:從上面我們可以看到,對象的創建其實包含了初始化和使用兩個階段。有了這個印象之後,我們就能開始今天的文章了。
  • Java類加載機制詳解
    以下是《深入理解Java虛擬機第二版》對類加載器機制的定義原文:虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
  • 大型企業JVM性能調優實戰Java垃圾收集器及gcroot
    02:JVM類加載機制大型企業JVM性能調優實戰之gcroot01:JVM類加載機制 - 類加載的生命周期概述java的class類文件實際上是二進位(字節碼)文件格式,class文件中包含了java虛擬機指令集和符號表以及若干其他輔助信息。
  • 聊到JVM(還怕面試官問JVM嗎?)
    1.啟動類(根)加載器:BootstrapClassLoader‍c++編寫,加載java核心庫 java.*,構造拓展類加載器和應用程式加載器。根加載器加載拓展類加載器,並且將拓展類加載器的父加載器設置為根加載器,然後再加載應用程式加載器,應將應用程式加載器的父加載器設置為拓展類加載器由於引導類加載器涉及到虛擬機本地實現細節,我們無法直接獲取到啟動類加載器的引用;這就是上面那個程序我們第三個結果為null的原因。加載文件存在位置2.
  • java類加載的過程概述
    本文為看雪論壇優秀文章看雪論壇作者ID:上火喝王老吉當程序要使用某個類時,如果該類還未被加載到內存中,則系統會通過加載
  • 面經手冊 · 第23篇《JDK、JRE、JVM,是什麼關係?》
    以及如下重要的組件:java – 運行工具,運行 .class 的字節碼javac– 編譯器,將後綴名為.java的原始碼編譯成後綴名為.class的字節碼javadoc – 文檔生成器,從源碼注釋中提取文檔,注釋需符合規範jar – 打包工具,將相關的類文件打包成一個文件appletviewer – 運行和調試applet程序的工具,不需要使用瀏覽器javah – 從Java類生成C頭文件和
  • 兩道面試題,帶你解析Java類加載機制
    其實這種面試題考察的就是你對Java類加載機制的理解。如果你對Java加載機制不理解,那麼你是無法解答這道題目的。這篇文章,我將通過對Java類加載機制的講解,讓你掌握解答此類題目的方法。Java類加載機制的七個階段當我們的Java代碼編譯完成後,會生成對應的 class 文件。
  • 我們寫的Java代碼是怎麼運行起來的?
    實際上並不是你寫的代碼真的可以運行到任何計算機上,而是計算機之上又封裝了一個虛擬的運行環境jvm,代碼實際運行在jvm上,jvm對底層計算機做了封裝,所以jvm我們可以理解為 "用於運行Java代碼的一個虛擬的機器"。
  • 面試官:為什麼java中靜態方法不能調用非靜態方法或變量?
    這個可能很多人之前學習jvm的時候都會遇到,屬於一個小問題,寫這篇文章的原因是我在看java相關的面試題目中遇到的,因此順手總結一下:一、例子我們先看效果:我們在靜態方法我們反過來看看:反過來沒有一點問題,接下來我們解釋一下原因:二、原因解釋我們需要首先知道的是靜態方法和靜態變量是屬於某一個類,而不屬於類的對象。我們不直接講原因,先從jvm說起:這是一張類加載的生命周期圖。
  • java class loader
    當你需要用java語言進行開發時,了解java類加載如何工作是很有幫助的。對類加載過程有基本的理解可以幫助Java程式設計師處理多種ClassLoader相關的異常。類加載器委派模型 java類的加載由類加載器(CL)來執行,CL負責將類載入到JVM中。簡單的應用程式可以利用java平臺自帶的類加載工具來進行載入,而更複雜的應用傾向於定義自己的CL。java中所有的CL被組織成樹結構。
  • 想理解JVM看了這篇文章,就知道了!
    2|0JVM介紹2|1什麼是JVM作為java工程師,對於jvm肯定不陌生。JVM是Java Virtual Machine的縮寫,通俗來說也就是運行java代碼的容器。當項目啟動時,會根據jvm相關配置參數,在計算機的內存中開啟一片空間用於運行JVM。
  • Java 的類加載過程
    Java 的類加載過程分為三個主要步驟:加載、連結、初始化,其中連結又分為驗證,準備和解析。
  • java中包名不能以java開頭
    java中自己寫的類的包名為什麼不能以java開頭?這是因為jvm在加載類的時候,連接階段,會做安全校驗,包名startsWith("java.")在運行期會報錯。具體是在ClassLoader.java中的preDefineClass方法:if ((name != null) && name.startsWith("java."))