JVM-白話聊一聊JVM類加載和雙親委派機制源碼解析

在這裏插入圖片描述


Java 執行代碼的大致流程

我們先回顧下Java 執行代碼的大致流程

在這裏插入圖片描述

假設要執行A類的main方法

  1. 啓動虛擬機 (C++負責創建) 【windows : bin/java.exe調用 jvm.dll Linux : java 調用 libjvm.so 】
  2. 創建一個引導類加載器實例 (C++實現)
  3. C++ 調用Java代碼,創建JVM啓動器,實例sun.misc.Launcher 【這貨由引導加載器負責加載創建其他類加載器】
    在這裏插入圖片描述

在這裏插入圖片描述

  1. sun.misc.Launcher.getLauncher() 獲取運行類自己的加載器ClassLoader --> 是AppClassLoader , 通過上圖源碼可知

  2. 獲取到ClassLoader後調用loadClass(“A”)方法加載運行的類A

  3. 加載完成執行A類的main方法

  4. 程序運行結束

  5. JVM銷燬


類加載loadClass的步驟

其中最核心的方法 loadClass ,其實現我們常說的雙親委派機制 ,我們後面展開。

在這裏插入圖片描述

我們先白話一下類加載的幾個步驟

加載 ----> 驗證 ----> 準備 ----> 解析 ----> 初始化 ----> 使用 ----> 卸載

談及比較多的是前五個 ,我們來捋一捋哈 ,不要嘗試死記硬背,嘗試去理解它的邏輯

  1. 加載: 我們說jvm執行的java字節碼,編譯後在磁盤上,總得讀取這個字節碼文件吧 ,通過啥讀 IO唄 , 所以第一步肯定是加載字節碼文件
  2. 驗證 : JVM總不能說讀到啥就直接運行了吧,你外面有個A.class 裏面是一堆JVM規範不認識的內容,也執行不了啊 。 符合JVM規範才能執行後續的步驟,所以第二步是 校驗字節碼文件的正確性
  3. 準備 : 給類的靜態變量分配內存,並賦予默認值。 我們的類裏,可能會包含一些靜態變量吧 。 比如 public static final int a = 12; 得給a分配個默認值 0 ,再比如 public static User user = new User(); 給 static的變量 User分配內存,並賦默認值null (final修飾的常量,直接賦值)
  4. 解析 : 這個地方不是很好理解, 解析是什麼意思呢?將符號引用替換爲直接引用。 符號引用 ? 直接引用? what ? ------------- 我們的類的靜態方法 比如main方法 其實在Java中有個叫法 都是叫符號 。 這個階段就會吧 一些靜態方法(符號引用,比如剛纔說的main方法)替換爲指向數據所存內存的指針或者句柄等(直接引用)【找到具體在內存中的位置】。 這個就是靜態鏈接過程(在類加載期間完成)。 動態鏈接是在程序運行期間完成的將符號引用替換爲直接引用 (比如某個普通方法的調用)
  5. 初始化: 上面的步驟完事兒以後,這一步主要是對類的靜態變量初始化爲指定的值,執行靜態代碼塊。 比如剛纔第二步的 public static final int a = 12; ,第二步給static變量賦了默認值,這一步就該把12賦值給它了。 還有 static的 User public static User user = new User(); 實例化User

類加載器和雙親委派機制

剛纔說了類加載器中loadClass方法實現了雙親委派的機制,那我們需要先了解下有哪幾種類加載器

主要有4種

  1. 引導類加載器:負責加載支撐JVM運行的位於JRE的lib目錄下的核心類庫,比如rt.jar、charsets.jar等
  2. 擴展類加載器:負責加載支撐JVM運行的位於JRE的lib目錄下的ext擴展目錄中的JAR類包
  3. 應用程序類加載器:負責加載ClassPath路徑下的類包,主要就是加載我們應用中自己寫的那些類
  4. 自定義加載器:負責加載用戶自定義路徑下的類包

我們來看看 幾種不同的類加載器


public class ClassLoadTest {

    public static void main(String[] args) {
        // 核心rt.jar中的類加載器 是C++加載的,因此這裏爲null 
        System.out.println(String.class.getClassLoader());
        // 擴展包的加載器 ExtClassLoader
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
        // 應用加載器 AppClassLoader
        System.out.println(ClassLoadTest.class.getClassLoader());


        System.out.println("");

        // 獲取系統ClassLoader
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        // appClassLoader的父加載器
        ClassLoader extClassLoader = appClassLoader.getParent();
        // extClassLoader的父加載器
        ClassLoader boostrapClassLoader = extClassLoader.getParent();

        System.out.println("the bootstrapLoader : " + boostrapClassLoader);
        System.out.println("the extClassloader : " + extClassLoader);
        System.out.println("the appClassLoader : "+ appClassLoader);


        System.out.println("");

        System.out.println("==============bootstrapLoader加載的文件====================");

        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urLs.length; i++) {
            System.out.println(urLs[i]);
        }
        System.out.println("");


        System.out.println("==============extClassloader加載的文件====================");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println("");


        System.out.println("==============appClassLoader 加載的文件====================");
        System.out.println(System.getProperty("java.class.path"));
    }
}

輸出

null
sun.misc.Launcher$ExtClassLoader@29453f44
sun.misc.Launcher$AppClassLoader@18b4aac2

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@29453f44
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2

==============bootstrapLoader加載的文件====================
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/resources.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/rt.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/sunrsasign.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jsse.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jce.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/charsets.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jfr.jar
file:/E:/Program%20Files/Java/jdk1.8.0_161/jre/classes

==============extClassloader加載的文件====================
E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

==============appClassLoader 加載的文件====================
E:\Program Files\Java\jdk1.8.0_161\jre\lib\charsets.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\deploy.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\access-bridge-64.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\cldrdata.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\dnsns.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\jaccess.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\jfxrt.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\localedata.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\nashorn.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunec.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunjce_provider.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunmscapi.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\sunpkcs11.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\zipfs.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\javaws.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jce.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jfr.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jfxswt.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\jsse.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\management-agent.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\plugin.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\resources.jar;E:\Program Files\Java\jdk1.8.0_161\jre\lib\rt.jar;D:\IdeaProjects\GOF23\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar


看看appClassLoader 咋加載這麼多? 其實它並沒有加載這麼多,除了 D:\IdeaProjects\GOF23\target\classes; 是它加載的,剩下的都是他的父加載器給他乾的。


sun.misc.Launcher源碼解析

JVM啓動時,C++會實例化JVM啓動器實例sun.misc.Launcher ,所以很有必要研究一下Launcher的源碼 。

Launcher實例化

 private static Launcher launcher = new Launcher();

採用了 餓漢模式 靜態域的方式 實現了單例模式 ,保證一個JVM虛擬機內只有一個sun.misc.Launcher實例。

Launcher 構造函數

實例化,調用構造函數,我們看下它的構造函數幹了啥?

  public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
           //構造擴展類加載器,在構造的過程中將其父加載器設置爲null
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try { 
             //構造應用類加載器,在構造的過程中將其父加載器設置爲ExtClassLoader,
             //Launcher的loader屬性值是AppClassLoader,我們一般都是用這個類加載器來加載我們自己寫的應用程序
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
          .....
         .....
         .....
        }

    }

Launcher構造方法內部, 創建了兩個類加載器,分別是sun.misc.Launcher.ExtClassLoader(擴展類加載器)和sun.misc.Launcher.AppClassLoader(應用類加載器)。

JVM默認使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實例加載我們的應用程序。


雙親委派機制 源碼解析

在這裏插入圖片描述

雙親委派過程

通俗的說: 當我們需要加載某個類時會先委託父加載器尋找目標類,找不到再委託上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標類,則在自己的類加載路徑中查找並載入目標類。

舉個例子,我們有個類A.class ,最先會找應用程序類加載器AppClassLoader 加載,AppClassLoader 會先委託擴展類加載器ExtClassLoader加載,擴展類加載器再委託引導類加載器BootClassLoader,頂層引導類加載器BootClassLoader在自己的類加載路徑裏 沒找到A類,則向下退回加載A類的請求,擴展類加載器ExtClassLoader收到回覆就自己加載,在自己的類加載路徑裏找了半天也沒找到A類,又向下退回A類的加載請求給應用程序類加載器AppClassLoader ,應用程序類加載器 在自己的類加載路徑裏找A類,結果找到了就自己加載了。。


源碼解析 ClassLoader#loadClass

loadClass實現了雙親委派的功能,我們有必要好好的研究一下

既然都是委託向上查找,那我們來看下應用程序類加載器AppClassLoader加載類的雙親委派機制源碼,AppClassLoader的loadClass方法最終會調用其父類ClassLoader的loadClass方法

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) {
                long t0 = System.nanoTime();
                try {
                
                    //如果當前加載器父加載器不爲空則委託父加載器加載該類
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else { 
                       //如果當前加載器父加載器爲空則委託引導類加載器加載該類
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                     
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    
                    //調用URLClassLoader的findClass方法在加載器的類路徑裏查找並加載該類
                    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;
        }
    }

看註釋~

總結一下幾個步驟

  1. 首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接返回。
  2. 如果此類沒有加載過,那麼,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用bootstrap類加載器來加載。
  3. 如果父加載器及bootstrap類加載器都沒有找到指定的類,那麼調用當前類加載器的findClass方法 【調用URLClassLoader的findClass方法在加載器的類路徑裏查找並加載該類】來完成類加載。

雙親委派機制的優點

  1. 沙箱安全機制:比如我們自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改
  2. 避免類的重複加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,保證被加載類的唯一性

全盤負責委託機制

這個比較好理解

“全盤負責”是指當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入。

比如我們的類 A中引用了 類B,由於全盤負責委託機制 ,類B也將有加載類A的加載器來加載,除非你顯示的使用另外一個ClassLoder。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章