完全搞懂JVM類加載器:基本概念

寫在前面

在Java面試中,在考察完項目經驗、基礎技術後,我會根據候選人的特色進行知識深度的考察,若是候選人簡歷上有寫JVM(Java虛擬機)相關的東西,那麼我經常會問一些JVM的問題。JVM的類加載機制是一個很經典的知識點,圍繞這個知識點能夠有下面這些難度不一樣的問題。html

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

在接下來的幾篇文章,我將跟讀者一塊兒從新梳理一遍類加載器的相關知識,爭取可以妥善解答上面列出的這些問題。java

基本概念篇

類的加載和卸載

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自身啓動的時候也會觸發類加載。api

經過JVM參數中加-verbose:class,能夠在應用啓動的時候打印類加載的過程,以下圖所示:併發

image-20191001224832934

初始化這個階段,JVM虛擬機給出了5種必須對類進行「初始化」的狀況oracle

  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個條件:jvm

  1. 該類全部的實例對象都已被回收;
  2. 該類的類加載器對象已經被回收;
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

類加載器的做用

類的加載是須要類加載器完成的,可是類加載器在JVM中的做用可不止這些。在JVM中,一個類的惟一性是須要這個類自己和類加載一塊兒才能肯定的,每一個類加載器都有一個獨立的命名空間。ide

不一樣的類加載器,即便是同一個類字節碼文件,最後再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, 0, 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
相關文章
相關標籤/搜索