1. 程式人生 > 程式設計 >徹底搞懂JVM類載入器:基本概念

徹底搞懂JVM類載入器:基本概念

寫在前面

在Java面試中,在考察完專案經驗、基礎技術後,我會根據候選人的特點進行知識深度的考察,如果候選人簡歷上有寫JVM(Java虛擬機器器)相關的東西,那麼我常常會問一些JVM的問題。JVM的類載入機制是一個很經典的知識點,圍繞這個知識點可以有下面這些難度不同的問題。

  1. 簡單講下JVM中的類載入過程
  2. JVM中的類載入和解除安裝的時機?
  3. 如何理解JVM中不同類載入器的概念和作用?
  4. 簡單講下JVM中的雙親委派模型?
  5. 什麼情況下會破壞雙親委派模型?為什麼?可否舉個例子?
  6. Tomcat中的類載入機制有了解嗎?為什麼這麼設計?
  7. 實際開發中有遇到哪些類載入器相關的問題?你又是如何解決的?
  8. JVM之上的弱型別語言例如Groovy是如何實現?簡單講下動態類載入機制?

在接下來的幾篇文章,我將跟讀者一起重新梳理一遍類載入器的相關知識,爭取能夠妥善解答上面列出的這些問題。

基本概念篇

類的載入和解除安裝

JVM是虛擬機器器的一種,它的指令集語言是位元組碼,位元組碼構成的檔案是class檔案。平常我們寫的Java檔案,需要編譯為class檔案才能交給JVM執行。可以這麼說:C語言程式碼——>二進位制檔案——>計算機硬體,就相當於Java程式碼——>位元組碼檔案——>JVM。JVM將指定的class檔案讀取到記憶體裡,並執行該class檔案裡的Java程式的過程,就稱之為類的載入

;反之,將某個class檔案的執行時資料從JVM中移除的過程,就稱之為類的解除安裝

class檔案的執行時資料就是C++物件,也稱為kclass物件,這些執行時資料在JDK7之前是放在永久代(PermGen),JDK8之後則放在元空間(Metaspace)。

類的生命週期

Java類從被虛擬機器器載入開始,到解除安裝出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段;其中驗證、準備和解析又統稱為連線(Linking)階段。

Java類的生命週期

類的載入的時機

虛擬機器器規範並未嚴格規定類載入的時機,跟具體的JVM虛擬機器器有關。類載入的最佳時機是解析Java位元組碼類檔案中常量池符號的時候,Class.forName()、ClassLoader.loadClass()、反射API和JNI_FindClass都可以觸發類載入,Hot JVM自身啟動的時候也會觸發類載入。

通過JVM引數中加-verbose:class,可以在應用啟動的時候列印類載入的過程,如下圖所示:

image-20191001224832934

初始化這個階段,JVM虛擬機器器給出了5種必須對類進行“初始化”的情況

  1. 使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位的時候、呼叫一個類的靜態方法的時候;
  2. 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則要先觸發其初始化;
  3. 當初始化一個類的時候,如果發現其父類還沒有被初始化,則要先初始化其父類;
  4. 當虛擬機器器啟動時,使用者需要指定一個執行的主類(包含main方法的那個類),則虛擬機器器會優先初始化這個主類;
  5. 在JDK1.7以後,動態語言支援的時候,如果一個java.lang.invoke.MethodHandle例項最後的結果是要執行第1種情況的操作,則也要進行初始化。

類的解除安裝時機

類的解除安裝跟採用的垃圾收集演演算法有關,在CMS中有兩種方法解除安裝不必要的類,一種是等到元空間(Metaspace)滿了的時候觸發FGC,另一種是使用跟CMS併發收集演演算法類似的方式,不過對於元空間的閾值和觸發CMS併發收集的閾值是獨立的。更具體的可以參考之前的文章:CMS學習筆記。在這裡,我們只需要記住,JVM中一個類的解除安裝要滿足下面這3個條件:

  1. 該類所有的例項物件都已被回收;
  2. 該類的類載入器物件已經被回收;
  3. 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

類載入器的作用

類的載入是需要類載入器完成的,但是類載入器在JVM中的作用可不止這些。在JVM中,一個類的唯一性是需要這個類本身和類載入一起才能確定的,每個類載入器都有一個獨立的名稱空間。

不同的類載入器,即使是同一個類位元組碼檔案,最後再JVM裡的類物件也不是同一個,下面的程式碼展示了這個結論:

package jvm;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException,IllegalAccessException,InstantiationException {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if (inputStream == null) {
                    return super.loadClass(name);
                }
                try {
                    byte[] b = new byte[inputStream.available()];
                    inputStream.read(b);
                    return defineClass(name,b,b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };

        Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof jvm.ClassLoaderTest);

        ClassLoaderTest classLoaderTest = new ClassLoaderTest();
        System.out.println(classLoaderTest.getClass());
        System.out.println(classLoaderTest instanceof jvm.ClassLoaderTest);
    }
}複製程式碼

上述程式碼的執行結果是:

可以看出,程式碼中使用自定義類載入器(myLoader)載入的jvm.ClassLoaderTest類和通過應用程式類載入器載入的類不是同一個類。綜上,類載入器在JVM中的作用有:

  1. 將類的位元組碼檔案從JVM外部載入到記憶體中
  2. 確定一個類的唯一性
  3. 提供隔離特性,為中介軟體開發者提供便利,例如Tomcat

總結

今天的文章,應該可以回答文章開始提出的前兩個問題,下篇再會。

參考資料

  1. jrebel.com/rebellabs/d…
  2. stackoverflow.com/questions/2…
  3. docs.oracle.com/javase/9/do…
  4. www.ibm.com/developerwo…
  5. blogs.oracle.com/sundararaja…
  6. 《深入理解Java虛擬機器器》
  7. 《揭祕Java虛擬機器器》
  8. 《Java效能權威指南》
    ***
    本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。javaadu