類加載器詳解


2 章 類加載器詳解

微信搜 : 全棧小劉 ,獲取 文章pdf版本

1、內存結構概述

如果自己想手寫一個Java虛擬機的話,主要考慮哪些結構呢?

  1. 類加載器
  2. 執行引擎

完整框圖:

2、類加載子系統

類加載器子系統作用

  1. 類加載器子系統負責從文件系統或者網絡中加載Class文件,class文件在文件開頭有特定的文件標識。
  2. ClassLoader只負責class文件的加載,至於它是否可以運行,則由Execution Engine決定。
  3. 加載的類信息存放於一塊稱為方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)

class --> Java.lang.Class

  1. class file存在於本地硬盤上,可以理解為設計師畫在紙上的模板,而最終這個模板在執行的時候是要加載到JVM當中來根據這個文件實例化出n個一模一樣的實例。
  2. class file加載到JVM中,被稱為DNA元數據模板,放在方法區
  3. 在.class文件–>JVM–>最終成為元數據模板,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。

3、類加載過程

3.1、類加載過程概述

  • 看代碼
public class HelloLoader {
    public static void main(String[] args) {
        System.out.println("謝謝ClassLoader加載我....");
        System.out.println("你的大恩大德,我下輩子再報!");
    }
}
  • 它的加載過程是怎么樣的呢?

    • 執行 main() 方法(靜態方法)就需要先加載承載類 HelloLoader
    • 加載成功,則進行鏈接、初始化等操作,完成后調用 HelloLoader 類中的靜態方法 main
    • 加載失敗則拋出異常

  • 完整的流程圖如下所示: *加載 --> 鏈接(驗證 --> 准備 --> 解析) --> 初始化

3.2、加載階段

加載流程

  1. 通過一個類的全限定名獲取定義此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化為 方法區的運行時數據結構
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口

加載class文件的方式

  1. 從本地系統中直接加載
  2. 通過網絡獲取,典型場景:Web Applet
  3. 從zip壓縮包中讀取,成為日后jar、war格式的基礎
  4. 運行時計算生成,使用最多的是:動態代理技術
  5. 由其他文件生成,典型場景:JSP應用從專有數據庫中提取.class文件,比較少見
  6. 從加密文件中獲取,典型的防Class文件被反編譯的保護措施

3.3、鏈接階段

  • *鏈接分為三個子階段:驗證 --> 准備 --> 解析

3.3.1、驗證(Verify)

驗證

  1. 目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全
  2. 主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

舉例

  • 使用 BinaryViewer 查看字節碼文件,其開頭均為 CAFE BABE ,如果出現不合法的字節碼文件,那么將會驗證不通過

3.3.2、准備(Prepare)

准備

  1. 為類變量分配內存並且設置該類變量的默認初始值,即零值
  2. 這里不包含用final修飾的static,因為final在編譯的時候就會分配好了默認值,准備階段會顯式初始化
  3. 注意:這里不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一起分配到Java堆中

舉例

  • 代碼:變量a在准備階段會賦初始值,但不是1,而是0,在初始化階段會被賦值為 1
public class HelloApp {
    private static int a = 1;

    public static void main(String[] args) {
        System.out.println(a);
    }
}

3.3.3、解析(Resolve)

解析

  1. 將常量池內的符號引用轉換為直接引用的過程
  2. 事實上,解析操作往往會伴隨着JVM在執行完初始化之后再執行
  3. 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機規范》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
  4. 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

符號引用

  • 反編譯 class 文件后可以查看符號引用

3.4、初始化階段

初始化階段

  1. 初始化階段就是執行類構造器方法 <clinit>()</clinit> 的過程
  2. 此方法不需定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合並而來。也就是說, 當我們代碼中包含static變量的時候,就會有clinit方法
  3. ** <clinit>()</clinit> 方法中的指令按語句在源文件中出現的順序執行**
  4. <clinit>()</clinit>不同於類的構造器。(關聯:構造器是虛擬機視角下的 <init>()</init>
  5. 若該類具有父類,JVM會保證子類的 <clinit>()</clinit> 執行前,父類的 <clinit>()</clinit> 已經執行完畢
  6. 虛擬機必須保證一個類的 <clinit>()</clinit> 方法在多線程下被同步加鎖

IDEA 中安裝 JClassLib 插件

在 IDEA 中安裝 JClassLib 插件后,重啟 IDEA 生效

  • 選中對應的 Java 類文件,注意:不是字節碼文件~!
  • 點擊【View --> Show Bytecode With jclasslib】即可查看反編譯后的代碼

當我們代碼中包含static變量的時候,就會有clinit方法

示例 1:無 static 變量

  • 代碼
public class ClinitTest {
    private int a = 1;

    public static void main(String[] args) {
        int b = 2;
    }
}
  • 並沒有生成 clinit 方法

示例 2:有 static 變量

  • 代碼
public class ClinitTest {

    private int a = 1;
    private static int c = 3;

    public static void main(String[] args) {
        int b = 2;
    }

}
  • 在 clinit 方法中初始化靜態變量的值為 3

構造器方法中指令按語句在源文件中出現的順序執行

示例 1

  • 代碼:
public class ClassInitTest {
    private static int num = 1;
    private static int number = 10;

    static {
        num = 2;
        number = 20;
        System.out.println(num);

    }

    public static void main(String[] args) {
        System.out.println(ClassInitTest.num);
        System.out.println(ClassInitTest.number);
    }
}
  • 靜態變量 number 的值變化過程如下
    • 准備階段時:0
    • 執行靜態變量初始化:10
    • 執行靜態代碼塊:20

示例 1

  • 代碼
public class ClassInitTest {
   private static int num = 1;

   static{
       num = 2;
       number = 20;
       System.out.println(num);

   }

   private static int number = 10;

    public static void main(String[] args) {
        System.out.println(ClassInitTest.num);
        System.out.println(ClassInitTest.number);
    }
}
  • 靜態變量 number 的值變化過程如下
    • 准備階段時:0
    • 執行靜態代碼塊:20
    • 執行靜態變量初始化:10

構造器是虛擬機視角下的 <init>()</init>

  • 代碼
public class ClinitTest {

    private int a = 1;
    private static int c = 3;

    public static void main(String[] args) {
        int b = 2;
    }

    public ClinitTest(){
        a = 10;
        int d = 20;
    }

}
  • 在構造器中:
    • 先將類變量 a 賦值為 10
    • 再將局部變量賦值為 20

若該類具有父類,JVM會保證子類的 <clinit>()</clinit> 執行前,父類的 <clinit>()</clinit> 已經執行完畢

  • 代碼
public class ClinitTest1 {
    static class Father{
        public static int A = 1;
        static{
            A = 2;
        }
    }

    static class Son extends Father{
        public static int B = A;
    }

    public static void main(String[] args) {

        System.out.println(Son.B);
    }
}
  • 如上代碼,加載流程如下:
    • 首先,執行 main() 方法需要加載 ClinitTest1 類
    • 獲取 Son.B 靜態變量,需要加載 Son 類
    • Son 類的父類是 Father 類,所以需要先執行 Father 類的加載,再執行 Son 類的加載

虛擬機必須保證一個類的 <clinit>()</clinit> 方法在多線程下被同步加鎖

  • 代碼
public class DeadThreadTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName() + "開始");
            DeadThread dead = new DeadThread();
            System.out.println(Thread.currentThread().getName() + "結束");
        };

        Thread t1 = new Thread(r, "線程1");
        Thread t2 = new Thread(r, "線程2");

        t1.start();
        t2.start();
    }
}

class DeadThread {
    static {
        if (true) {
            System.out.println(Thread.currentThread().getName() + "初始化當前類");
            while (true) {

            }
        }
    }
}
  • 程序卡死,分析原因:
    • 兩個線程同時去加載 DeadThread 類,而 DeadThread 類中靜態代碼塊中有一處死循環
    • 先加載 DeadThread 類的線程搶到了同步鎖,然后在類的靜態代碼塊中執行死循環,而另一個線程在等待同步鎖的釋放
    • 所以無論哪個線程先執行 DeadThread 類的加載,另外一個類也不會繼續執行

4、類加載器的分類

4.1、類加載器概述

類加載器的分類

  1. JVM支持兩種類型的類加載器 。分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)
  2. 從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是Java虛擬機規范卻沒有這么定義,而是 將所有派生於抽象類ClassLoader的類加載器都划分為自定義類加載器
  3. 無論類加載器的類型如何划分,在程序中我們最常見的類加載器始終只有3個,如下所示
  4. 這里的四者之間是包含關系,不是上層和下層,也不是子父類的繼承關系。

為什么 ExtClassLoader 和 AppClassLoader 都屬於自定義加載器

  • 規范定義:所有派生於抽象類ClassLoader的類加載器都划分為自定義類加載器
  • ExtClassLoader 繼承樹

  • AppClassLoader 繼承樹

  • 代碼
    • 我們嘗試獲取引導類加載器,獲取到的值為 null ,這並不代表引導類加載器不存在, 因為引導類加載器右 C/C++ 語言,我們獲取不到
    • 兩次獲取系統類加載器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,這說明 *系統類加載器是全局唯一的
public class ClassLoaderTest {
    public static void main(String[] args) {

        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);

        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);

        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);

        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);

    }
}

4.2、虛擬機自帶的加載器

4.2.1、啟動類加載器

啟動類加載器(引導類加載器,Bootstrap ClassLoader)

  1. 這個類加載使用C/C++語言實現的,嵌套在JVM內部
  2. 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類
  3. 並不繼承自java.lang.ClassLoader,沒有父加載器
  4. 加載擴展類和應用程序類加載器,並作為他們的父類加載器(當他倆的爹)
  5. 出於安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類

4.2.2、擴展類加載器

擴展類加載器(Extension ClassLoader)

  1. Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現
  2. 派生於ClassLoader類
  3. 父類加載器為啟動類加載器
  4. 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。如果用戶創建的JAR放在此目錄下,也會自動由擴展類加載器加載

4.2.3、系統類加載器

應用程序類加載器(系統類加載器,AppClassLoader)

  1. Java語言編寫,由sun.misc.LaunchersAppClassLoader實現
  2. 派生於ClassLoader類
  3. 父類加載器為擴展類加載器
  4. 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫
  5. 該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載
  6. 通過classLoader.getSystemclassLoader()方法可以獲取到該類加載器

代碼舉例說明

  • 代碼
public class ClassLoaderTest1 {
    public static void main(String[] args) {

        System.out.println("**********啟動類加載器**************");

        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) {
            System.out.println(element.toExternalForm());
        }

        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);

        System.out.println("***********擴展類加載器*************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }

        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);

    }
}
  • System.out.println(classLoader); 輸出 null ,再次證明我們無法獲取到啟動類加載器
**********&#x542F;&#x52A8;&#x7C7B;&#x52A0;&#x8F7D;&#x5668;**************
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/classes
null
***********&#x6269;&#x5C55;&#x7C7B;&#x52A0;&#x8F7D;&#x5668;*************
C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@7ea987ac

4.3、用戶自定義類加載器

為什么需要自定義類加載器?

在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式。那為什么還需要自定義類加載器?

  1. 隔離加載類
  2. 修改類加載的方式
  3. 擴展加載源
  4. 防止源碼泄漏

如何自定義類加載器?

  1. 開發人員可以通過繼承抽象類java.lang.ClassLoader類的方式,實現自己的類加載器,以滿足一些特殊的需求
  2. 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
  3. 在編寫自定義類加載器時,如果沒有太過於復雜的需求,可以直接繼承URIClassLoader類,這樣就可以避免自己去編寫findclass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。

代碼示例

public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }

    private byte[] getClassFromCustomPath(String name) {

        return null;
    }

    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("One", true, customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.4、關於 ClassLoader

ClassLoader 類介紹

  • ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)

  • sun.misc.Launcher 它是一個java虛擬機的入口應用

獲取 ClassLoader 途徑

  • 獲取途徑:

  • 代碼示例:
public class ClassLoaderTest2 {
    public static void main(String[] args) {
        try {

            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader);

            ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
            System.out.println(classLoader1);

            ClassLoader classLoader2 = ClassLoader.getSystemClassLoader();
            System.out.println(classLoader2);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

5、雙親委派機制

5.1、雙親委派機制原理

雙親委派機制的原理

Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象。而且 加載某個類的class文件時,Java虛擬機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式

  1. 如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委托給父類的加載器去執行;
  2. 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器;
  3. 如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式。
  4. 父類加載器一層一層往下分配任務,如果子類加載器能加載,則加載此類,如果將加載任務分配至系統類加載器也無法加載此類,則拋出異常

5.2、雙親委派機制代碼示例

代碼示例

舉例 1 :

  • 代碼:我們自己建立一個 java.lang.String 類,寫上 static 代碼塊
package java.lang;

public class String {
    static{
        System.out.println("我是自定義的String類的靜態代碼塊");
    }
}
  • 在另外的程序中加載 String 類,看看加載的 String 類是 JDK 自帶的 String 類,還是我們自己編寫的 String 類
public class StringTest {

    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
        System.out.println("hello,atguigu.com");

        StringTest test = new StringTest();
        System.out.println(test.getClass().getClassLoader());
    }
}
  • 程序並沒有輸出我們靜態代碼塊中的內容,可見仍然加載的是 JDK 自帶的 String 類

舉例 2 :

  • 代碼:在我們自己的 String 類中整個 main() 方法
package java.lang;

public class String {
    static{
        System.out.println("我是自定義的String類的靜態代碼塊");
    }

    public static void main(String[] args) {
        System.out.println("hello,String");
    }
}
  • 由於雙親委派機制找到的是 JDK 自帶的 String 類,在那個 String 類中並沒有 main() 方法

舉例 3 :

  • 代碼:在 java.lang 包下整個 ShkStart 類
package java.lang;

public class ShkStart {
    public static void main(String[] args) {
        System.out.println("hello!");
    }
}
  • 出於保護機制,java.lang 包下不允許我們自定義類

舉例 4 :

當我們加載jdbc.jar 用於實現數據庫連接的時候

  1. 首先我們需要知道的是 jdbc.jar是基於SPI接口進行實現的
  2. 所以在加載的時候,會進行雙親委派,最終從根加載器中加載 SPI核心類,然后再加載SPI接口類
  3. 接着在進行反向委托,通過線程上下文類加載器進行實現類 jdbc.jar的加載。

5.3、雙親委派機制優勢

雙親委派機制的優勢

通過上面的例子,我們可以知道,雙親機制可以

  1. 避免類的重復加載
  2. 保護程序安全,防止核心API被隨意篡改
  3. 自定義類:java.lang.String 沒有屌用
  4. 自定義類:java.lang.ShkStart(報錯:阻止創建 java.lang開頭的類)

6、沙箱安全機制

  1. 自定義String類時:在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的文件(rt.jar包中java.lang.String.class),報錯信息說沒有main方法,就是因為加載的是rt.jar包中的String類。
  2. 這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。

7、其他

如何判斷兩個class對象是否相同?

在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:

  1. 類的完整類名必須一致,包括包名
  2. 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
  3. 換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的

對類加載器的引用

  1. JVM必須知道一個類型是由啟動加載器加載的還是由用戶類加載器加載的
  2. 如果一個類型是由用戶類加載器加載的,那么JVM會將這個類加載器的一個引用作為類型信息的一部分保存在方法區中
  3. 當解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的

類的主動使用和被動使用

Java程序對類的使用方式分為:主動使用和被動使用。主動使用,又分為七種情況:

  1. 創建類的實例
  2. 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  3. 調用類的靜態方法
  4. 反射(比如:Class.forName("com.atguigu.Test"))
  5. 初始化一個類的子類
  6. Java虛擬機啟動時被標明為啟動類的類
  7. JDK7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化

除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化,即不會執行初始化階段(不會調用 clinit() 方法和 init() 方法)

你只管學習,我來負責記筆記😲 關注公眾號! ,更多筆記,等你來拿,謝謝




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM