JVM 類加載與反射

作者:aruba
鏈接:https://www.jianshu.com/p/0bd9498863f5

一、類加載過程

有了前面的瞭解,我們知道 Java 文件先要編譯成 class 文件,再由 JVM 加載 class 到方法區成爲類元信息,最後實例化 class 對象,加載類的過程又可以細分爲:加載、連接、初始化、使用、卸載

1. 加載(Loading)

Java 編譯爲 class 文件後,在使用類時,JVM 如果沒有加載過 class,則會先加載 class 文件,加載可以用讀文件操作來理解,就是將文件內容加載到內存中,轉化爲類元信息,作爲方法區這個類的各種數據訪問入口,並實例化 Class 對象,存放在堆中

2. 連接(Linking)

2.1 驗證(Verifivation)

class 文件爲字節碼,根據字節碼對應表進行驗證,如:對該 class 文件進行標誌頭校驗,class 文件的前 4 個字節都是 “0xCAFEBABE”

2.2 準備(Preparation)

根據字節碼,如果有靜態成員變量,那麼在方法區爲它們分配內存,並將內存清零(類似 c 語言 memset 函數),如果是靜態常量(final 修飾),那麼此時就會賦值,字符串比較特殊,它會分配在字符串常量池中

2.3 解析(Resolution)

根據字節碼對照表把 Constant Pool Table 中的符號轉換成直接引用
每個符號爲一個指針,解析時,將符號指向對應的內存首地址(變量、函數、函數類型結構體等)
棧幀中的動態鏈接也是使用這種機制,一個方法對應一個指針,指向了常量池中的符號,符號指向一個方法,來執行方法中的代碼

下面 class 文件反編譯的內容,可以作爲參考:

public class Hello {
    public String name = "aaa";
    private final static int nameConst = 123;
    private static int nameStatic = 1234;

    private int test() {
        int a = 3;
        int b = 4;

        return a + b;
    }

    public int test(int a) {
        int b = 4;

        return a + b;
    }

}
Classfile /C:/Users/tyqhc/Documents/javaworkspace/myJava/out/production/myJava/Hello.class
  Last modified 2021-10-13; size 651 bytes
  MD5 checksum 57a1c2ff200580304191ccda4feaea70
  Compiled from "Hello.java"
public class Hello
  SourceFile: "Hello.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#29         //  java/lang/Object."<init>":()V
   #2 = String             #30            //  aaa
   #3 = Fieldref           #5.#31         //  Hello.name:Ljava/lang/String;
   #4 = Fieldref           #5.#32         //  Hello.nameStatic:I
   #5 = Class              #33            //  Hello
   #6 = Class              #34            //  java/lang/Object
   #7 = Utf8               name
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               nameConst
  #10 = Utf8               I
  #11 = Utf8               ConstantValue
  #12 = Integer            123
  #13 = Utf8               nameStatic
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               LHello;
  #21 = Utf8               test
  #22 = Utf8               ()I
  #23 = Utf8               a
  #24 = Utf8               b
  #25 = Utf8               (I)I
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               Hello.java
  #29 = NameAndType        #14:#15        //  "<init>":()V
  #30 = Utf8               aaa
  #31 = NameAndType        #7:#8          //  name:Ljava/lang/String;
  #32 = NameAndType        #13:#10        //  nameStatic:I
  #33 = Utf8               Hello
  #34 = Utf8               java/lang/Object
{
  public java.lang.String name;
    flags: ACC_PUBLIC

  public Hello();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String aaa
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      11     0  this   LHello;

  public int test(int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_4
         1: istore_2
         2: iload_1
         3: iload_2
         4: iadd
         5: ireturn
      LineNumberTable:
        line 19: 0
        line 21: 2
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       6     0  this   LHello;
               0       6     1     a   I
               2       4     2     b   I

  static {};
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: sipush        1234
         3: putstatic     #4                  // Field nameStatic:I
         6: return
      LineNumberTable:
        line 9: 0
}

3. 初始化(Initialization)

類初始化階段是類加載過程的最後一步。在前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的 java 程序代碼 (字節碼)。
在準備階段,只對靜態成員變量進行了內存分配和內存初始化,這些內存。初始化階段纔會對成員變量進行賦值,相當於執行構造函數中的內容,以及執行 static 代碼塊中的內容

4. 爲對象分配內存的兩種方式

二、類加載時機

我們瞭解了類加載的流程後,試想下,什麼時候會觸發類的加載呢?

三、類加載器 - 雙親委派機制

類加載時,如果以前加載過,那麼就不需要加載該類,實現這個機制的,就是雙親委派
子加載器不斷往上詢問是否加載過,再有頂至下加載該類,可以加載就直接加載,否則往下委派加載

四、反射

反射是 Java 中一種機制,它能夠幫助我們動態的使用一個類,其本質就是獲取類元信息,並通過符號引用來操作內存或調用方法
例子使用的類如下:

public class Hello {
    public String name;
    private String namePrivate;

    public int test() {
        int a = 3;
        int b = 4;

        return a + b;
    }

    public int test(int a) {
        int b = 4;

        return a + b;
    }

}

1. 獲取 Class 對象

JVM 加載類後,會在方法區存在一份類元信息,我們可以通過以下方法獲取它:

2. 獲取方法和調用方法

獲取到 Class 後,我們就可以通過以下兩種方式,獲取方法對象 Method,並通過 Method 的 invoke 方法反向調用方法

    //getMethod只能獲取公有方法,但是也可以獲取父類公有方法
    Method method = helloClass.getMethod("test", int.class);
    method.invoke(h, 5);
    //getDeclaredMethod可以獲取該類中的所有方法,但是不可以獲取父類方法
    Method method = helloClass.getDeclaredMethod("test");
    method.setAccessible(true);
    method.invoke(h);

3. 獲取構造方法和通過 Class 實例化對象

Class 對象還可以獲取構造方法:

    Constructor<Hello> constructor = helloClass.getConstructor();

同樣的,私有構造方法可以通過 getDeclaredConstructor 方法獲取,setAccessible(true) 後纔可以調用

我們可以通過構造方法來實例化對象:

    Hello h2 = constructor.newInstance();

還可以通過 Class 直接實例化,該方式只能是無參構造

    Hello h3 = helloClass.newInstance();

4. 獲取屬性和設置屬性

通過以下兩種方式,獲取屬性 Field 對象,並通過 Filed 的 set 方法,來爲對象設置新值

    Field name = helloClass.getField("name");
    name.set(h, "hello");
    Field name = helloClass.getDeclaredField("namePrivate");
    name.setAccessible(true);
    name.set(h, "helloPrivate");
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Tnj_xJqZOSqoun_iBa-9fQ