天天看點

JVM HotSpot架構分析

作者:航淳技術

JVM HotSpot架構

JVM HotSpot架構分析

JVM HotSpot架構思維導圖

JVM HotSpot架構分析

JVM HotSpot之類加載子系統

一個Java檔案從編碼完成到最終執行,一般主要包括兩個過程

  • 編譯,即把我們寫好的java檔案,通過javac指令編譯成位元組碼,也就是我們常說的.class檔案。
  • 運作,則是把編譯生成的.class檔案交給Java虛拟機(JVM)執行。

而我們所說的類加載過程即是指JVM虛拟機把.class檔案中類資訊加載進記憶體,并進行解析生成對應的class對象的過程。

舉個通俗點的例子來說,JVM在執行某段代碼時,遇到了class A, 然而此時記憶體中并沒有class A的相關資訊,于是JVM就會到相應的class檔案中去尋找class A的類資訊,并加載進記憶體中,這就是我們所說的類加載過程。

由此可見,JVM不是一開始就把所有的類都加載進記憶體中,而是隻有第一次遇到某個需要運作的類時才會加載,且隻加載一次。

** 下面以HelloWorld.java為類剖析類加載子系統原理**

package com.example.demo;

public class HelloWorld {
    private static  final int a = 1;
    private static int b = 2;
    public static void main(String[] args) {
         int c = 3;
         test1();
         new HelloWorld().test2();
    }

    public static void test1(){
        System.out.println(a);
    }

    public void test2(){
        int f = 5;
        System.out.println(f);
    }
}
           

分析位元組碼

  • 用windows下生成檢視class位元組碼過程,打開cmd指令行工具:

    編譯:cd E:\study\sourcecode\demo\target\classes\com\example\demo: javac HelloWorld.java

    檢視位元組碼cd E:\study\sourcecode\demo\target\classes\com\example\demo: javap HelloWorld.class > HelloWorld.txt 将生成的位元組碼輸出到HelloWorld.txt

Classfile /E:/study/sourcecode/demo/target/classes/com/example/demo/HelloWorld.class
  Last modified 2021-10-1; size 892 bytes
  MD5 checksum 725ca55458c6d045ad2c25c4b7b8477b
  Compiled from "HelloWorld.java"
public class com.example.demo.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#34         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#35         // com/example/demo/HelloWorld.test1:()V
   #3 = Class              #36            // com/example/demo/HelloWorld
   #4 = Methodref          #3.#34         // com/example/demo/HelloWorld."<init>":()V
   #5 = Methodref          #3.#37         // com/example/demo/HelloWorld.test2:()V
   #6 = Fieldref           #38.#39        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #40.#41        // java/io/PrintStream.println:(I)V
   #8 = Fieldref           #3.#42         // com/example/demo/HelloWorld.b:I
   #9 = Class              #43            // java/lang/Object
  #10 = Utf8               a
  #11 = Utf8               I
  #12 = Utf8               ConstantValue
  #13 = Integer            1
  #14 = Utf8               b
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/example/demo/HelloWorld;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               c
  #27 = Utf8               MethodParameters
  #28 = Utf8               test1
  #29 = Utf8               test2
  #30 = Utf8               f
  #31 = Utf8               <clinit>
  #32 = Utf8               SourceFile
  #33 = Utf8               HelloWorld.java
  #34 = NameAndType        #15:#16        // "<init>":()V
  #35 = NameAndType        #28:#16        // test1:()V
  #36 = Utf8               com/example/demo/HelloWorld
  #37 = NameAndType        #29:#16        // test2:()V
  #38 = Class              #44            // java/lang/System
  #39 = NameAndType        #45:#46        // out:Ljava/io/PrintStream;
  #40 = Class              #47            // java/io/PrintStream
  #41 = NameAndType        #48:#49        // println:(I)V
  #42 = NameAndType        #14:#11        // b:I
  #43 = Utf8               java/lang/Object
  #44 = Utf8               java/lang/System
  #45 = Utf8               out
  #46 = Utf8               Ljava/io/PrintStream;
  #47 = Utf8               java/io/PrintStream
  #48 = Utf8               println
  #49 = Utf8               (I)V
{
  public com.example.demo.HelloWorld();
    descriptor: ()V
    flags: 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/example/demo/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_3
         1: istore_1
         2: invokestatic  #2                  // Method test1:()V
         5: new           #3                  // class com/example/demo/HelloWorld
         8: dup
         9: invokespecial #4                  // Method "<init>":()V
        12: invokevirtual #5                  // Method test2:()V
        15: return
      LineNumberTable:
        line 7: 0
        line 8: 2
        line 9: 5
        line 10: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     c   I
    MethodParameters:
      Name                           Flags
      args

  public static void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
         7: return
      LineNumberTable:
        line 13: 0
        line 14: 7

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_5
         1: istore_1
         2: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 17: 0
        line 18: 2
        line 19: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/example/demo/HelloWorld;
            2       8     1     f   I

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_2
         1: putstatic     #8                  // Field b:I
         4: return
      LineNumberTable:
        line 5: 0
}
SourceFile: "HelloWorld.java"
           
  • 用IDEA工具,菜單路徑: view > show ByteCode顯示位元組碼,安裝jclasslib分析位元組碼:view > show ByteCode jclasslib

位元組碼内容分析是一項極其複雜的過程,主要是分析16進制檔案與位元組碼的映射, 下一節JVM運作區域再做部分分析 ,本節内容主要是想說明,類是如何加載.class位元組碼,内容有哪些

JVM如何啟動及類加載分析

  1. 根據JVM記憶體配置要求,為JVM申請特定大小的記憶體空間;
  2. 建立一個引導類加載器執行個體,初步加載系統類到記憶體方法區區域中;
  3. 建立JVM 啟動器執行個體 Launcher,并取得類加載器ClassLoader;
  4. 使用上述擷取的ClassLoader執行個體加載我們定義的 org.luanlouis.jvm.load.Main類;
  5. 加載完成時候JVM會執行Main類的main方法入口,執行Main類的main方法;
  6. 結束,java程式運作結束,JVM銷毀
  • Step 1. 根據JVM記憶體配置要求,為JVM申請特定大小的記憶體空間
  • 所有的類的定義資訊都會被加載到方法區中。
  • Step 2. 建立一個引導類加載器執行個體,初步加載系統類到記憶體方法區區域中
  • JVM申請好記憶體空間後,JVM會建立一個引導類加載器(Bootstrap Classloader)執行個體,引導類加載器是使用C++語言實作的,負責加載JVM虛拟機運作時所需的基本系統級别的類,如java.lang.String, java.lang.Object等等。 引導類加載器(Bootstrap Classloader)會讀取 {JRE_HOME}/lib 下的jar包和配置,然後将這些系統類加載到方法區内。
  • 也可以使用參數 -Xbootclasspath 或 系統變量sun.boot.class.path來指定的目錄來加載類
  • 引導類加載JVM記憶體格局如下:
  • Step 3. 建立JVM 啟動器執行個體 Launcher,并取得類加載器ClassLoader

    JVM虛拟機調用已經加載在方法區的類sun.misc.Launcher的靜态方法getLauncher(), 擷取sun.misc.Launcher,執行個體如下:

sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); 
//擷取Java啟動器 
ClassLoader classLoader = launcher.getClassLoader();  
           

//擷取類加載器ClassLoader用來加載class到記憶體來

sun.misc.Launcher 使用了單例模式設計,保證一個JVM虛拟機内隻有一個sun.misc.Launcher執行個體。

在Launcher的内部,其定義了兩個類加載器(ClassLoader),分别是sun.misc.Launcher.ExtClassLoader和sun.misc.Launcher.AppClassLoader,這兩個類加載器分别被稱為拓展類加載器(Extension ClassLoader) 和 應用類加載器(Application ClassLoader)

JVM HotSpot架構分析

應用類加載器加載類流程圖如下:

JVM HotSpot架構分析

雙親委派模型(parent-delegation model):

上面讨論的應用類加載器AppClassLoader的加載類的模式就是我們常說的雙親委派模型(parent-delegation model).

對于某個特定的類加載器而言,應該為其指定一個父類加載器,當用其進行加載類的時候:

  • 委托父類加載器幫忙加載;
  • 父類加載器加載不了,則查詢引導類加載器有沒有加載過該類;
  • 如果引導類加載器沒有加載過該類,則目前的類加載器應該自己加載該類;
  • 若加載成功,傳回 對應的Class 對象;若失敗,抛出異常“ClassNotFoundException”。
public Class<?> loadClass(String name) throws ClassNotFoundException {
     return loadClass(name, false);
 }
 protected Class<?> loadClass(String name, boolean resolve)         throws ClassNotFoundException
 {        
     synchronized (getClassLoadingLock(name)) {
     // 首先,檢查是否已經被目前的類加載器記載過了,如果已經被加載,直接傳回對應的Class<T>執行個體
     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) {
             // ClassNotFoundException thrown if class not found                     // from the non-null parent class loader
         }                 // 父加載器加載失敗,并且沒有被引導類加載器加載,則嘗試該類加載器自己嘗試加載
         //
         if (c == null) {
             // If still not found, then invoke findClass in order                     // to find the class.
             long t1 = System.nanoTime();                     // 自己嘗試加載
             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. 父類加載器(parent classloader):它可以替子加載器嘗試加載類
  2. 引導類加載器(bootstrap classloader): 子類加載器隻能判斷某個類是否被引導類加載器加載過,而不能委托它加載某個類;換句話說,就是子類加載器不能接觸到引導類加載器,引導類加載器對其他類加載器而言是透明的

    一般情況下,雙親委派模型圖:

  • Step 4. 使用類加載器ClassLoader加載HelloWorld類

    通過launcher.getClassLoader()方法傳回AppClassLoader執行個體,接着就是AppClassLoader加載HelloWorld類的時候了

ClassLoader classloader = launcher.getClassLoader();
//取得AppClassLoader類 
 classLoader.loadClass("com.example.demo.HelloWorld");//加載自定義類
           

定義的com.example.demo.HelloWorld類被編譯成com.example.demo.HelloWorld class二進制檔案,這個class檔案中有一個叫常量池(Constant Pool)的結構體來存儲該class的常亮資訊,常量池中有CONSTANT_CLASS_INFO類型的常量,表示該class中聲明了要用到那些類:

JVM HotSpot架構分析

類子系統加載完成後JVM記憶體結構圖如下:

JVM HotSpot架構分析

Step 5. 使用Main類的main方法作為程式入口運作程式

Step 6. 方法執行完畢,JVM銷毀,釋放記憶體

繼續閱讀