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 加載類後,會在方法區存在一份類元信息,我們可以通過以下方法獲取它:
-
Class.forName("xx") : 根據包名加類名獲取
-
Hello.class:通過類. class 獲取
-
h.getClass():實例化後,對象的 getClass 方法獲取
2. 獲取方法和調用方法
獲取到 Class 後,我們就可以通過以下兩種方式,獲取方法對象 Method,並通過 Method 的 invoke 方法反向調用方法
- getMethod:獲取類中的公有方法和父類的公有方法
//getMethod只能獲取公有方法,但是也可以獲取父類公有方法
Method method = helloClass.getMethod("test", int.class);
method.invoke(h, 5);
- getDeclaredMethod:獲取該類中的所有方法
//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 方法,來爲對象設置新值
- getField:獲取公有屬性和父類共有屬性
Field name = helloClass.getField("name");
name.set(h, "hello");
- getDeclaredField:獲取類的所有屬性
Field name = helloClass.getDeclaredField("namePrivate");
name.setAccessible(true);
name.set(h, "helloPrivate");
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Tnj_xJqZOSqoun_iBa-9fQ