徹底剖析JVM類加載機制

本文仍然基於JDK8版本,從JDK9模塊化器,類加載器有一些變動。

0 javac編譯

java代碼

public class Math {
    public static final int initData = 666;

    public static User user = new User();

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        System.out.println("end");
    }
}

javac 編譯,javap -v -p 查看class文件

Classfile /F:/workspace/advanced-java/target/classes/com/lzp/java/jvm/classloader/Math.class

// 第1部分,描述信息:大小、修改時間、md5值等
  Last modified 2022年1月8日; size 1006 bytes
  MD5 checksum 4cece4543963b23a98cd219a59c1887c
  Compiled from "Math.java"

// 第2部分,描述信息:編譯版本
public class com.lzp.java.jvm.classloader.Math
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // com/lzp/java/jvm/classloader/Math
  super_class: #11                        // java/lang/Object
  interfaces: 0, fields: 2, methods: 4, attributes: 1
  
// 第3部分,常量池信息
Constant pool:
   #1 = Methodref          #11.#39        // java/lang/Object."<init>":()V
   #2 = Class              #40            // com/lzp/java/jvm/classloader/Math
   #3 = Methodref          #2.#39         // com/lzp/java/jvm/classloader/Math."<init>":()V
   #4 = Methodref          #2.#41         // com/lzp/java/jvm/classloader/Math.compute:()I
   #5 = Fieldref           #42.#43        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #44            // end
   #7 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #47            // com/lzp/java/jvm/classloader/User
   #9 = Methodref          #8.#39         // com/lzp/java/jvm/classloader/User."<init>":()V
  #10 = Fieldref           #2.#48         // com/lzp/java/jvm/classloader/Math.user:Lcom/lzp/java/jvm/classloader/User;
  #11 = Class              #49            // java/lang/Object
  #12 = Utf8               initData
  #13 = Utf8               I
  #14 = Utf8               ConstantValue
  #15 = Integer            666
  #16 = Utf8               user
  #17 = Utf8               Lcom/lzp/java/jvm/classloader/User;
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               Lcom/lzp/java/jvm/classloader/Math;
  #25 = Utf8               compute
  #26 = Utf8               ()I
  #27 = Utf8               a
  #28 = Utf8               b
  #29 = Utf8               c
  #30 = Utf8               main
  #31 = Utf8               ([Ljava/lang/String;)V
  #32 = Utf8               args
  #33 = Utf8               [Ljava/lang/String;
  #34 = Utf8               math
  #35 = Utf8               MethodParameters
  #36 = Utf8               <clinit>
  #37 = Utf8               SourceFile
  #38 = Utf8               Math.java
  #39 = NameAndType        #18:#19        // "<init>":()V
  #40 = Utf8               com/lzp/java/jvm/classloader/Math
  #41 = NameAndType        #25:#26        // compute:()I
  #42 = Class              #50            // java/lang/System
  #43 = NameAndType        #51:#52        // out:Ljava/io/PrintStream;
  #44 = Utf8               end
  #45 = Class              #53            // java/io/PrintStream
  #46 = NameAndType        #54:#55        // println:(Ljava/lang/String;)V
  #47 = Utf8               com/lzp/java/jvm/classloader/User
  #48 = NameAndType        #16:#17        // user:Lcom/lzp/java/jvm/classloader/User;
  #49 = Utf8               java/lang/Object
  #50 = Utf8               java/lang/System
  #51 = Utf8               out
  #52 = Utf8               Ljava/io/PrintStream;
  #53 = Utf8               java/io/PrintStream
  #54 = Utf8               println
  #55 = Utf8               (Ljava/lang/String;)V
{
// 第四部分,變量信息
  public static final int initData;
    descriptor: I
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 666

  public static com.lzp.java.jvm.classloader.User user;
    descriptor: Lcom/lzp/java/jvm/classloader/User;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC

  public com.lzp.java.jvm.classloader.Math();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lzp/java/jvm/classloader/Math;
// 第五部分,方法信息
  public int compute();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 9: 0
        line 10: 2
        line 11: 4
        line 12: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/lzp/java/jvm/classloader/Math;
            2      11     1     a   I
            4       9     2     b   I
           11       2     3     c   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/lzp/java/jvm/classloader/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: ldc           #6                  // String end
        18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        21: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 18: 13
        line 19: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  args   [Ljava/lang/String;
            8      14     1  math   Lcom/lzp/java/jvm/classloader/Math;
    MethodParameters:
      Name                           Flags
      args

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #8                  // class com/lzp/java/jvm/classloader/User
         3: dup
         4: invokespecial #9                  // Method com/lzp/java/jvm/classloader/User."<init>":()V
         7: putstatic     #10                 // Field user:Lcom/lzp/java/jvm/classloader/User;
        10: return
      LineNumberTable:
        line 6: 0
}

方法中的#1/2,可以到ConstantPool找到對應符號。

參考字節碼指令表:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html。

1 類加載過程

j經典的類加載過程如下圖,包括加載、鏈接、初始化三部分。

image

1.1 加載class文件

字節碼文件位於磁盤,當使用到某個類(例如,調用main()方法,new新對象),在磁盤中查找並通過IO讀取文件的二進制流,轉爲方法區數據結構,並存放到方法區,在Java堆中產生 java.lang.Class對象。Class對象是可以方法區的訪問入口,用於Java反射機制,獲取類的各種信息。

1.2 鏈接過程

驗證:驗證class文件是不是符合規範

  1. 文件格式的驗證。驗證是否以0XCAFEBABE開頭,版本號是否合理

  2. 元數據驗證。是否有父類,是否繼承了final類(final類不能被繼承),非抽象類實現了所有抽象方法。

  3. 字節碼驗證。(略)

  4. 符號引用驗證。常量池中描述類是否存在,訪問的方法或字段是否存在且有足夠的權限。

-Xverify:none  // 取消驗證

準備:爲類的靜態變量分配內存,初始化爲系統的初始值

final static修飾的變量:直接賦值爲用戶定義的值,比如 private final static int value=123,直接賦值123。

private static int value=123,該階段的值依然是0。

解析:符號引用轉換成直接引用(靜態鏈接)

Java代碼中每個方法、方法參數都是符號,類加載放入方法區的常量池Constant pool中。

符號引用:應該可以理解成常量池中的這些字面量。【可能沒理解對】

直接引用:符號對應代碼被加載到JVM內存中的位置(指針、句柄)。

靜態鏈接過程在類加載時完成,主要轉換一些靜態方法。動態鏈接是在程序運行期間完成的將符號引用替換爲直接引用。

1.3 初始化(類初始化clinit-->初始化init)

執行< clinit>方法, clinit方法由編譯器自動收集類裏面的所有靜態變量的賦值動作及靜態語句塊合併而成,也叫類構造器方法

  • 初始化的順序和源文件中的順序一致

  • 子類的< clinit>被調用前,會先調用父類的< clinit>

  • JVM會保證clinit方法的線程安全性

初始化時,如果實例化一個新對象,會調用<init>方法對實例變量進行初始化,並執行對應的構造方法內的代碼。

類加載過程是懶加載的,用到纔會加載。

初始化示例

public class JVMTest2 {
    static {
        System.out.println("JVMTest2靜態塊");
    }

    {
        System.out.println("JVMTest2構造塊");
    }

    public JVMTest2() {
        System.out.println("JVMTest2構造方法");
    }

    public static void main(String[] args) {
        System.out.println("main方法");
        new Sub();
    }
}

class Super {
    static {
        System.out.println("Super靜態代碼塊");
    }

    public Super() {
        System.out.println("Super構造方法");
    }

    {
        System.out.println("Super普通代碼塊");
    }
}

class Sub extends Super {
    static {
        System.out.println("Sub靜態代碼塊");
    }

    public Sub() {
        System.out.println("Sub構造方法");
    }

    {
        System.out.println("Sub普通代碼塊");
    }
}

JVMTest2靜態塊
main方法
Super靜態代碼塊
Sub靜態代碼塊
Super普通代碼塊
Super構造方法
Sub普通代碼塊
Sub構造方法

執行main方法,並不需要創建JVMTest2實例。

對於普通代碼塊,以前認爲是和clinit一樣順序加載。其實是不一樣的,普通代碼塊編譯時對賦值語句和其他語句分別做了優化,如下賦值語句優化爲int i = 1; 打印語句優化爲構造方法的第一句。

源代碼

public class JVMTest1 {
    int i;
    {
        i = 1;
        System.out.println("JVMTest1構造塊");
    }
    public JVMTest1(){
        System.out.println("JVMTest1構造方法");
    }
}

反編譯後的代碼

public class JVMTest1 {
    int i = 1;

    public JVMTest1() {
        System.out.println("JVMTest1構造塊");
        System.out.println("JVMTest1構造方法");
    }
}

2 類加載器

查看當前JDK類加載器

public class PrintJDKClassLoader {
    public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        ClassLoader parent = systemClassLoader.getParent();
        System.out.println(parent);
        ClassLoader parentParent = parent.getParent();
        System.out.println(parentParent);
    }
}

// JDK8
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@28a418fc
null
// JDK11
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1324409e
null

2.1 類加載器(JDK8)

類加載器初始化過程:Java通過調用jvm.dll文件創建JVM,創建一個引導類加載器(由C++實現),通過JVM啓動器(sun.misc.Launcher)加載擴展類加載器和應用類加載器。

  • 啓動類加載器:負責加載lib目錄下的核心類庫。作爲JVM的一部分,由C++實現。

  • 擴展類/平臺類加載器:負責加載lib目錄下的ext擴展目錄中的JAR 類包。

  • 應用程序類加載器:負責加載用戶類路徑ClassPath路徑下的類包,主要就是加載用戶自己寫的類。

  • 自定義類加載器:負責加載用戶自定義路徑下的類包。

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

// Launcher構造方法
public Launcher() {
    Launcher.ExtClassLoader var1;
    // 構造擴展類加載器,設置類加載器parent屬性設爲null。
    var1 = Launcher.ExtClassLoader.getExtClassLoader();
    // 構造應用類加載器,設置類加載器parent屬性爲擴展類加載器。
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    Thread.currentThread().setContextClassLoader(this.loader);
    // 權限校驗代碼..
    }
}

2.2 雙親委派模型

類加載器採用三層、雙親委派模型,類加載器的父子關係不是繼承關係,而是組合關係。除了啓動類加載器外,其他類加載器都是繼承自ClassLoader類。

image

工作過程:類加載器收到類加載請求,首先判斷類是否已經加載,如果未被加載,嘗試將請求向上委派給父類加載器加載。當父類加載器無法完成加載任務,再由子類加載器嘗試加載。

// ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 檢查類是否已被加載
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) { 
                    // 非啓動類加載器
                    c = parent.loadClass(name, false);
                } else { 
                    // 啓動類加載器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父類加載器無法加載指定類
            }

            if (c == null) {
                // 調用當前類加載器的findClass方法進行類加載
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

爲什麼使用雙親委派模型,感覺走了彎路?

雙親委派模型下,類加載請求總會被委派給最上層的啓動類加載器。對於未加載的類來說,需要從底層走到頂層;如果用戶定義的類已經被加載過,則不需要委派過程。

使用雙親委派機制有下面幾個好處:

  • 沙箱安全機制,防止核心類庫代碼被篡改。
  • 避免類重複加載,父類加載器加載過,子類加載器不需要再次加載。

全盤負責委託機制

全盤負責 :即是當一個classloader加載一個Class的時候,這個Class所依賴的和引用的其它Class 通常 也由這個classloader負責載入。 委託機制 :先讓parent(父)類加載器 尋找,只有在parent找不到的時候才從自己的類路徑中去尋找。

參考Launcher構造方法

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

自定義類加載器

自定義類加載器操作主要是繼承ClassLoader類,重寫上面源碼中的findClass(name)方法。

public class CustomClassLoaderTest {
    static class CustomClassLoader extends ClassLoader {
        private String classFilePath;

        public CustomClassLoader(String classFilePath) {
            this.classFilePath = classFilePath;
        }
		// 載入class數據流
        private byte[] loadClassFile(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classFilePath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException{
            try {
                byte[] data = loadClassFile(name);
                // 加載--鏈接--初始化等邏輯
                return defineClass(name,data,0,data.length);
            } catch (Exception e) {
                throw new ClassNotFoundException();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        CustomClassLoader classLoader = new CustomClassLoader("F:");
        Class<?> clazz = classLoader.loadClass("com.lzp.java.jvm.classloader.JVMTest");
        Object instance = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("add", null);
        System.out.println(method.invoke(instance));
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

自定義類加載器的父加載器是應用類加載器。CustomClassLoader是使用AppClassLoader運行的,自然而然是父類加載器。

打破雙親委派機制

在一些場景下,打破雙親委派是必要的。例如Tomcat中可能有多個應用,引用了不同的Spring版本。打破雙親委派,可以實現應用隔離。

JVM使用loadClass方法實現雙親委派機制。重寫loadClass方法,便可以打破雙親委派機制。

直接刪除雙親委派代碼是不可行的,Java代碼繼承自Object,總會需要雙親委派來加載核心代碼。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 非自定義的類還是走雙親委派加載
            if (!name.equals("com.lzp.java.jvm.classloader.JVMTest")) {
                c = this.getParent().loadClass(name);
            } else {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

注:JDK自帶的核心庫代碼,是不允許自行配置修改的。例如,不可以將Object.class拷出來執行。沙箱隔離。

image

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