關於 Java 中 Runtime.class.getClass() 的細節分析
* 在之前的《淺析Java序列化和反序列化》一文的Payload構造章節中出現了一大堆的Class
、Method
和Object
,讓很多程式碼基礎較弱的同學一臉懵逼。其中一個比較詭異的邏輯Runtime.class.getClass()
,有朋友問它的結果為什麼是java.lang.Class
。對於這個問題,有Java語言基礎的同學一般會回答『物件的型別本來就是Class
,而Class
也是物件,它的型別當然也是Class
』,道理沒錯,但仔細想想,這還真是一個挺有意思的問題。
關於
Class
的名稱
我們先重寫一下這個問題的程式碼:
Class rt = Runtime.class; Class clz = rt.getClass();
通過斷點除錯觀察變數,rt
和clz
同樣都是Class
物件,但rt
無論是列印輸出還是呼叫getTypeName()
得到的都是『java.lang.Runtime』,而clz
則是『java.lang.Class』。
為什麼不一樣?難道Runtime
是Class
的子類?當然不是,Runtime
可是Object
的親兒子。
機智的你一定會跟進Class
中看看它的toString()
和getTypeName()
兩個方法的程式碼邏輯,原來它們都是呼叫getName()
返回由這個Class
所表示的物件的名稱。
關於
.class
和
getClass()
由此可知,new Object().getClass()
得到的應該是名稱為『java.lang.Object』的Class
,記作class java.lang.Object
(以下類似)
,而Runtime.class
拿到的Class
作為Object
的子類,呼叫getClass()
得到的卻是class java.lang.Class
。
因此,我們需要對比一下這兩種獲取Class
的方法的區別:
-
.class
,又稱『類字面量』,只能作用於類的關鍵字,返回編譯時確定的型別Object.class
-
getClass()
,Object
的例項方法,返回執行時確定的型別new Object().getClass()
在一般情況下,它倆的結果是可以相等的:
Object obj = new Object(); Object.class == obj.getClass();// true Object.class.equals(obj.getClass()); // true
但當存在多型時,後者的區別就體現出來了:
class gyyyy {} Object obj = new Object(); Object gy = new gyyyy(); obj.getClass(); // class java.lang.Object gy.getClass();// class gyyyy
讓我們回到最初的那個問題,答案已經呼之欲出了:Runtime.class
獲取的是class java.lang.Runtime
,而該Class
呼叫getClass()
時,執行時確定的型別為Class
而非方法擁有者Object
,所以得到的第二個Class
為class java.lang.Class
。
看到這,一定有同學開始罵我又在水文章了:褲子都脫了你就給我看這個?說來說去都是一堆廢話,跟沒說一樣。
別急,我們繼續。
JVM基礎
既然上面的兩種方法分別提到了編譯時和執行時,不妨讓我們站在JVM的角度再玩深一點。
先科普幾個JVM相關的基礎知識,讓大家有個整體概念,其他的內容如果在後續分析過程中遇到了再穿插介紹。
Classfile
每個類(包括內部類、匿名類、介面、註解、列舉和陣列等) 經過編譯後,都會單獨生成一個.class檔案,裡面是一堆用於表示和描述該類的位元組碼,Java規範中管它叫Classfile。
Classfile中的核心內容如下:
-
常量池(Constant Pool)
-
訪問許可權標識(Access Flags)
-
類(This Class)
-
父類(Super Class)
-
介面集合(Interfaces)
-
欄位集合(Fields)
-
方法集合(Methods)
-
屬性集合(Attributes)
其中,常量池裡存放了該類編譯前宣告和編譯中優化計算的所有值,包括原始型別和引用型別(符號引用) ,類相關資訊都以名稱和描述為主,但不涉及任何具體的值或引用 (都依賴常量池索引) 。屬性集合中則存放了類、欄位和方法所可能需要的屬性資訊,如類原始檔資訊、方法程式碼段、方法程式碼段的本地變量表等。
執行時記憶體基本結構
-
執行時資料區
-
執行緒(Threads)
-
幀(Frames)
-
本地變量表(Local Variables)
-
運算元棧(Operand Stacks)
-
程式計數器(Program Counter, PC)
-
JVM堆疊(JVM Stack)
-
堆(Heap)
-
類(Class)
-
執行時常量池(Run-Time Constant Pool)
-
方法區(Method Area)
-
物件(Objects)
-
執行緒共享
-
執行緒私有
其中,執行緒共享部分隨JVM啟動而建立,執行緒私有部分隨執行緒建立而建立。Frame中存放的是方法資料而非Class資料,但一般來說,Object和方法的程式碼實現中都會存放它所屬Class的引用。
需要注意的是,上面列出的Class和Object大致分別對應在Java程式碼中使用class
或interface
關鍵字宣告的類和根據它們建立的類例項,而Java語言規範中所描述的Class
和Object
嚴格意義上來說都屬於Class。
載入、連結和初始化
-
載入,是指根據指定名稱尋找並讀取Classfile,將其轉換成Class的過程
-
連結,是指解析Class中的符號引用,並轉換為執行時狀態的過程
-
初始化,是指執行Class的
<cinit>
方法的過程
在這個階段中,可以為Class建立一個新的java/lang/Class
的Object,在其中定義一個欄位中存放當前Class的引用,並將這個Object的引用放入Class中作為其類物件 (非JVM規範,由實現方自行決定)
,而這個所謂的類物件,就是我們最開始通過.class
和getClass()
獲取到的那個Class
物件。
方法執行過程
由於篇幅原因,這裡只簡單介紹例項方法的執行過程:
-
從常量池中取出方法引用,計算該方法引數個數
-
從運算元棧彈出當前類物件引用和其他引數,組成引數列表
-
為該方法建立新的
Frame
,將引數放入它的本地變量表中,將其壓入JVM棧頂 -
解析並執行該方法程式碼段的指令集
方法的執行結果並不會直接返回給呼叫方,而是由return
系列的指令將當前運算元棧頂元素取出,壓入JVM棧中呼叫方所屬Frame的運算元棧中。
刨根問底
現在,我們將示例程式碼放入main
函式中,這段程式碼經過編譯後會變成以下指令:
ldc #2 astore_1 aload_1 invokevirtual #3 astore_2 ...
(#x
代表常量池索引值,可能會因為示例程式碼差異而不同。如果使用鏈式結構Runtime.class.getClass()
,第2、3條指令會省略)
大致解釋一下:
-
ldc
指令會從常量池中取索引為2
的元素,此時取到的是名為java/lang/Runtime
的類引用型別常量,根據JVM規範的描述,如果是類引用型別常量,需要獲取它的類物件引用 (在前面載入、連結和初始化部分提到過的那個Object) ,再將其壓入運算元棧 (對應Runtime.class
) -
astore_1
指令會彈出運算元棧頂元素,放入本地變量表的1
位置 (0
位置是main
方法引數args
) ,此時該位置的變數名為rt
(對應Class rt =
) -
aload_1
指令會從本地變量表中讀取元素壓入運算元棧 (對應rt
) -
invokevirtual
指令會從常量池中取索引為3
的元素,此時取到的是名為java/lang/Object.getClass
的方法引用型別常量,再彈出運算元棧頂獲得之前ldc
得到的類物件引用作為第一個引數,為該方法建立新的Frame
並壓入JVM堆疊,執行該方法的指令集,return
時將結果壓入方法呼叫方的運算元棧 (對應.getClass()
) -
astore_2
指令會彈出棧頂元素,放入本地變量表的2
位置,此時該位置的變數名為clz
(對應Class clz =
)
由此,我們可以明確的知道變數rt
存放的是java/lang/Runtime
的類物件引用,變數clz
存放的是java/lang/Class
的類物件引用。由於類物件是在Class的連結過程中建立的,而在JVM中每個Class又是唯一的單例,因此同一個類以及它不同的例項獲取到的類物件都是同一個。
結論不變。