用Java實現JVM(二):支援介面、類和物件
1. 概述
我的 JVM 已經能夠執行HelloWorld
了,並且有了基本的 JVM 骨架,包括執行時資料結構的定義(棧、棧幀、運算元棧等),執行時的邏輯控制等。但它還沒有類和物件的概念,比如無法執行下面這更復雜的HelloWorld
:
public interface SpeakerInterface {
public void helloTo(String somebody);
}
public class Speaker implements SpeakerInterface{
private String hello = "";
Speaker(String hello){
this .hello = hello;
}
public void helloTo(String somebody){
System.out.println(this.hello +" "+ somebody);
}
}
public class Main{
private final static SpeakerInterface speaker = new Speaker("Hello");
public static void main(String[] args){
speaker.helloTo(args[0]);
}
}
要讓上述程式碼工作,將涉及到了:
類的初始化
類靜態成員的初始化,如類成員
Main.speaker
在何時初始化。物件初始化(例項化)
如
new Speaker("Hello")
如何執行,物件的成員(如private String hello = "";
)如何初始化。注意String
在JJvm 中被當做 Native 類,那麼 Native 類又如何初始化。物件屬性的操作
包括 Native 類和非 Native 類例項的屬性的操作,如訪問
Speaker.hello
。方法呼叫
包括例項方法、類方法、介面方法的呼叫。
2. 抽象
為了支援類和物件的概念,我在 JVM 層做了抽象,如下圖:
Java 類和物件Native 類和物件我定義了類和物件的基本形態(這裡只列出了介面的主要方法):
JvmClass
表示“類”,類提供例項化(
newInstance
)、獲取方法(getMethod
)、獲取屬性(getField
)和獲取父類(getSuperClass
)的方法。注意這裡的“例項化”指建立物件,但不呼叫物件的建構函式。物件的建構函式是在位元組碼指令中顯式呼叫的。JvmField
表示“屬性”, 提供獲取(
set
)和設定(get
)屬性的方法。JvmMethod
表示“方法”,提供方法呼叫(
call
)和獲取引數數量(getParameterCount
)方法。這裡會什麼會有“獲取引數數量”的方法?因為執行時,需要知道從運算元棧中推出幾個元素,作為方法呼叫的引數。JvmObject
表示“物件”,提供獲取父類物件(
getSuper
)和獲取當前類(getClazz
)的方法。如果一個類有多級繼承, 則這個類的例項中會包含多個 JvmObject 例項。如A --|> B --|> Object
, 那麼A
的例項 a,其內部有三個JvmObject
例項, 每一個JvmObject
例項維護自己所表示的類的屬性。
你可能注意到一點,這裡沒有提到介面interface
的概念。原因是 JVM 中並不需要太多關注介面,實際上為了讓示例能執行,和介面有關的就是操作碼 invokeinterface
。關於invokeinterface
將在後面說明。
3. 實現
基於前面定義的介面,再編寫兩套實現,分別表示原生類(JvmNative*)和Java 類(JvmOpcode*)。下面將以Java 類的實現為例,進行說明。
3.1. 類的初始化
類的初始化即呼叫類的<clinit>
方法, 如下面是示例Main
類的初始化方法的位元組碼:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #4 // class org/caoym/samples/sample2/Speaker
3: dup
4: ldc #5 // String Hello
6: invokespecial #6 // Method org/caoym/samples/sample2/Speaker."<init>":(Ljava/lang/String;)V
9: putstatic #2 // Field speaker:Lorg/caoym/samples/sample2/SpeakerInterface;
12: return
LineNumberTable:
line 5: 0
這段程式碼先例項化了Speaker
物件,然後將物件設定給類的靜態變數speaker
。關於物件的例項化過程,將在後面介紹。這裡我們先關注類的初始化。我為類JvmOpcodeClass
實現初始化程式碼:
public void clinit(Env env) throws Exception {
if(inited) return;
synchronized(this){ //類初始化方法需要保證執行緒安全
if(inited) return;
inited = true;
JvmOpcodeMethod method = methods.get(new AbstractMap.SimpleEntry<>("<clinit>", "()V"));
if(method != null){
method.call(env, null);
}
}
}
也就是找到<clinit>
方法,然後按正常方法的形式執行。關於類的初始化方法何時被執行,這裡摘錄了《Java 虛擬機器規範 (Java SE 7 版)》中的描述:
- 在執行下列需要引用類或介面的Java虛擬機器指令時:new,getstatic,putstatic 或 invokestatic。這些指令通過欄位或方法引用來直接或間接地引用其它類。執行上 面所述的 new 指令,在類或介面沒有被初始化過時就初始化它。執行上面的 getstatic, putstatic 或 invokestatic 指令時,那些解析好的欄位或方法中的類或介面如果還 沒有被初始化那就初始化它。
- 在初次呼叫java.lang.invoke.MethodHandle例項時,它的執行結果為通過Java 虛擬機器解析出型別是 2(REF_getStatic)、4(REF_putStatic)或者 6 (REF_invokeStatic)的方法控制代碼(§5.4.3.5)。
- 在呼叫JDK核心類庫中的反射方法時,例如,Class類或java.lang.reflect包。
- 在對於類的某個子類的初始化時。
- 在它被選定為Java虛擬機器啟動時的初始類(§5.2)時。
簡單說就是例項化、訪問屬性、呼叫方法、使用反射前,被初始化。
3.2. 物件初始化
還是先看示例Main
類的初始化方法的位元組碼
0: new #4 // class org/caoym/samples/sample2/Speaker
3: dup
4: ldc #5 // String Hello
6: invokespecial #6 // Method org/caoym/samples/sample2/Speaker."<init>":(Ljava/lang/String;)V
上述位元組碼對應的程式碼是
new Speaker("Hello");
為了讓位元組碼能夠執行,需要實現這些指令:
new
分配物件,也就建立我們的
JvmOpcodeObject
。指令實現如下:/** * 建立一個物件,並將其引用值壓入棧頂。 */ NEW(Constants.NEW){ @Override public void invoke(Env env, StackFrame frame, byte[] operands) throws Exception { // 獲取類資訊 int index = (operands[0] << 8)| operands[1]; ConstantPool.CONSTANT_Class_info info = (ConstantPool.CONSTANT_Class_info)frame.getConstantPool().get(index); // 根據類名載入類 JvmClass clazz = env.getVm().getClass(info.getName()); // 建立物件,並推入運算元棧 frame.getOperandStack().push(clazz.newInstance(env)); } },
ldc
將 int,float 或 String 型常量值從常量池中推送至棧頂。此處將常量“Hello”推入棧頂。
dup
複製棧頂數值並將複製值壓入棧頂。複製的目的是因為建構函式本身沒有返回值,
invokespecial
呼叫建構函式後將消耗掉運算元棧上的引用,所以需要事先備份一個。程式碼略。invokespecial
該指令用於呼叫超類構造方法、例項初始化方法或者私有方法。此處呼叫的是構造方法
<init>
。/** * 呼叫超類構造方法、例項初始化方法或者私有方法。 */ INVOKESPECIAL(Constants.INVOKESPECIAL){ @Override public void invoke(Env env, StackFrame frame, byte[] operands) throws Exception { // 獲取類和方法資訊 int arg = (operands[0]<<8)|operands[1]; ConstantPool.CONSTANT_Methodref_info info = (ConstantPool.CONSTANT_Methodref_info)frame.getConstantPool().get(arg); // 根據類名載入類 JvmClass clazz = env.getVm().getClass(info.getClassName()); // 根據方法名找到方法 JvmMethod method = clazz.getMethod( info.getNameAndTypeInfo().getName(), info.getNameAndTypeInfo().getType() ); // 從運算元棧中推出方法的引數 ArrayList<Object> args = frame.getOperandStack().multiPop(method.getParameterCount() + 1); Collections.reverse(args); Object[] argsArr = args.toArray(); JvmObject thiz = (JvmObject) argsArr[0]; // 根據類名確定是呼叫父類還是子類 while (!thiz.getClazz().getName().equals(clazz.getName())){ thiz = thiz.getSuper(); } method.call(env, thiz, Arrays.copyOfRange(argsArr,1, argsArr.length)); } }
再看Speaker
建構函式<init>
的位元組碼:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String
7: putfield #3 // Field hello:Ljava/lang/String;
10: aload_0
11: aload_1
12: putfield #3 // Field hello:Ljava/lang/String;
15: return
這裡比較特別的是Speaker
的建構函式中又呼叫了父類Object
的建構函式。
可以回過頭再看下invokespecial
指令的實現, 指令執行時,方法對應的類是確定的,比如此處是Speaker
的父類Object
,而不是Speaker
。執行過程中需要找到對應的類和例項,並呼叫其方法。前面介紹JvmObject
的時候,已經介紹過繼承的實現方式。以下為 JvmOpcodeObject
中表示繼承的實現:
private final JvmObject superObject;
public JvmOpcodeObject(Env env, JvmOpcodeClass clazz) throws IllegalAccessException, InstantiationException {
this.clazz = clazz;
JvmClass superClass = null;
try {
superClass = clazz.getSuperClass();
} catch (ClassNotFoundException e) {
throw new InstantiationException(e.getMessage());
}
superObject = superClass.newInstance(env);
...
}
另外Object
在 JJvm 中被視作原生類,所以我們又實現了一組JvmNative*
,用於操作原生類。
3.3. 類和物件屬性的操作
類的屬性儲存在 JvmOpcodeStaticField
中;物件的屬性儲存在JvmOpcodeObject
中,並通過JvmOpcodeObjectField
操作。
3.4. 方法呼叫
除了前面已經說明過的invokespecial
指令,還有invokestatic
:用於靜態方法呼叫;invokevirtual
:用於例項方法呼叫;invokeinterfac
:用於介面方法呼叫。除了invokeinterface
,其他指令實現與invokespecial
類似。
關於invokeinterface
,比如:
6: invokeinterface #3, 2 // InterfaceMethod org/caoym/samples/sample2/SpeakerInterface.helloTo:(Ljava/lang/String;)V
操作碼的第一個引數指定了介面方法, 第二個指定方法的引數個數。有了引數個數,就可以從操作棧中推出所有引數和方法對應的物件。然後根據繼承關係,遞迴查詢物件的類,直到找到匹配的方法。也就是說執行時可以不需要任何 interface 的資訊。
下面為invokeinterface
指令的實現:
INVOKEINTERFACE(Constants.INVOKEINTERFACE){
@Override
public void invoke(Env env, StackFrame frame, byte[] operands) throws Exception {
// 獲取介面和方法資訊
int arg = (operands[0]<<8)|operands[1];
ConstantPool.CONSTANT_InterfaceMethodref_info info
= (ConstantPool.CONSTANT_InterfaceMethodref_info)frame.getConstantPool().get(arg);
String interfaceName = info.getClassName();
String name = info.getNameAndTypeInfo().getName();
String type = info.getNameAndTypeInfo().getType();
// 獲取介面的引數數量
int count = 0xff&operands[2]; //TODO count代表引數個數,還是引數所佔的槽位數?
//從運算元棧中推出方法的引數
ArrayList<Object> args = frame.getOperandStack().multiPop(count + 1);
Collections.reverse(args);
Object[] argsArr = args.toArray();
JvmObject thiz = (JvmObject)argsArr[0];
JvmMethod method = null;
//遞迴搜尋介面方法
while(thiz != null){
if(thiz.getClazz().hasMethod(name, type)){
method = thiz.getClazz().getMethod(name, type);
break;
}else{
thiz = thiz.getSuper();
}
}
if(method == null){
throw new AbstractMethodError(info.toString());
}
// 執行介面方法
method.call(env, thiz, Arrays.copyOfRange(argsArr,1, argsArr.length));
}
4. 結束
使用新的 JJvm 執行文章開始處的示例,將得到以下輸出:
> org/caoym/samples/sample2/Main.<clinit>@0:NEW
> org/caoym/samples/sample2/Main.<clinit>@1:DUP
> org/caoym/samples/sample2/Main.<clinit>@2:LDC
> org/caoym/samples/sample2/Main.<clinit>@3:INVOKESPECIAL
> org/caoym/samples/sample2/Speaker.<init>@0:ALOAD_0
> org/caoym/samples/sample2/Speaker.<init>@1:INVOKESPECIAL
> org/caoym/samples/sample2/Speaker.<init>@2:ALOAD_0
> org/caoym/samples/sample2/Speaker.<init>@3:LDC
> org/caoym/samples/sample2/Speaker.<init>@4:PUTFIELD
> org/caoym/samples/sample2/Speaker.<init>@5:ALOAD_0
> org/caoym/samples/sample2/Speaker.<init>@6:ALOAD_1
> org/caoym/samples/sample2/Speaker.<init>@7:PUTFIELD
> org/caoym/samples/sample2/Speaker.<init>@8:RETURN
> org/caoym/samples/sample2/Main.<clinit>@4:PUTSTATIC
> org/caoym/samples/sample2/Main.<clinit>@5:RETURN
> org/caoym/samples/sample2/[email protected]:GETSTATIC
> org/caoym/samples/sample2/[email protected]:ALOAD_0
> org/caoym/samples/sample2/[email protected]:ICONST_0
> org/caoym/samples/sample2/[email protected]:AALOAD
> org/caoym/samples/sample2/[email protected]:INVOKEINTERFACE
> org/caoym/samples/sample2/[email protected]:GETSTATIC
> org/caoym/samples/sample2/[email protected]:NEW
> org/caoym/samples/sample2/[email protected]:DUP
> org/caoym/samples/sample2/[email protected]:INVOKESPECIAL
> org/caoym/samples/sample2/[email protected]:ALOAD_0
> org/caoym/samples/sample2/[email protected]:GETFIELD
> org/caoym/samples/sample2/[email protected]:INVOKEVIRTUAL
> org/caoym/samples/sample2/[email protected]:LDC
> org/caoym/samples/sample2/[email protected]:INVOKEVIRTUAL
> org/caoym/samples/sample2/[email protected]:ALOAD_1
> org/caoym/samples/sample2/[email protected]:INVOKEVIRTUAL
> org/caoym/samples/sample2/[email protected]:INVOKEVIRTUAL
> org/caoym/samples/sample2/[email protected]:INVOKEVIRTUAL
Hello World
> org/caoym/samples/sample2/[email protected]:RETURN
> org/caoym/samples/sample2/[email protected]:RETURN
符號“>”開始的行是執行日誌,日誌記錄了指令的執行步驟。
相關推薦
用Java實現JVM(二):支援介面、類和物件
1. 概述我的 JVM 已經能夠執行HelloWorld了,並且有了基本的 JVM 骨架,包括執行時資料結構的定義(棧、棧幀、運算元棧等),執行時的邏輯控制等。但它還沒有類和物件的概念,比如無法執行下面這更復雜的HelloWorld:public interface SpeakerInterface {
Java WebSocket程式設計(二):WebSocket實現主動推送互動
WebSocket協議 WebSocket協議通訊機制 WebSocket協議是獨立的、基於TCP的協議。其本質是先通過HTTP/HTTPS協議進行握手後建立一個用於交換資料的TCP連線,此後伺服器端與客戶器端通過此TCP連線進行實時通訊。 WebSocket開啟握手
深入理解JVM(二):Java記憶體區域
執行時資料區域 Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程序的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立和銷燬。根據《Java虛擬機
Java JVM(二):垃圾回收概念 與 GC 日誌
包括: 一. 垃圾回收基本概念 二. GC日誌一. 垃圾回收基本概念 在JVM 中,最需要進行回收的地方就是JVM 方法區 和 JVM 堆。1.1 可達性分析演算法 回收的時候,主要是根據可達性分析演算法。如果一個物件不可達,那麼就是可以回收
Java設計模式(二):單例模式的5種實現方式,以及在多執行緒環境下5種建立單例模式的效率
這段時間從頭溫習設計模式。記載下來,以便自己複習,也分享給大家。 package com.iter.devbox.singleton; /** * 餓漢式 * @author Shearer * */ public class SingletonDemo1 {
【怎樣寫程式碼】實現物件的複用 -- 享元模式(二):解決方案
如果喜歡這裡的內容,你能夠給我最大的幫助就是轉發,告訴你的朋友,鼓勵他們一起來學習。 If you like the content here, you can give me the greatest help is forwarding, tell you
java枚舉(二):即對java枚舉(一)中的例子進行拓展
枚舉/* 知識點:枚舉 枚舉是從java5開始提供的一種新的數據類型,是一個特殊的類,就是多個常量對象的集合 定義格式: [修飾符] enum 枚舉類名 { 常量A, 常量B, 常量C; } */ //定義枚舉 enum Weekday { Mond
Java並發(二):重排序
技術分享 安排 通過 mage 種類 操作 處理器 加載 str 在執行程序時為了提高性能,提高並行度,編譯器和處理器常常會對指令做重排序。重排序分三種類型: 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。 指令級並行的重排序。現代
Java 設計模式(二):工廠方法模式
參考連結:工廠方法模式-Factory Method Pattern 在介紹簡單工廠模式時提到簡單工廠模式存在一個很嚴重的問題,就是當系統中需要引入新產品時,如果靜態工廠方法是通過傳入引數的不同來建立不同的產品,這必定要修改工廠類的原始碼,將違背“開閉原則”,如何實現增加新產品而不影
Generic Netlink核心實現分析(二):通訊
前一篇博文中分析了Generic Netlink的訊息結構及核心初始化流程,本文中通過一個示例程式來了解Generic Netlink在核心和應用層之間的單播通訊流程。 示例程式:demo_genetlink_kern.c(核心模組)、demo_genetlink_
JVM(二):JVM類載入機制
如下圖所示,JVM類載入機制分為五個部分:載入,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。 載入 載入是類載入過程中的一個階段,這個階段會在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口。注意這裡不一
java Restful框架(二):jersey請求對映和頁面傳值
jersey的webservice開發基本上都是使用註解,接下來學習常用註解. 一.根資源類 [email protected]註解 @Path("/hello") public class HelloWorldController { @G
基於MCMS用Java開發網站(二)
上篇說到,將下載下來的mcms匯入 注意幾點: mcms預設編碼是utf-8 所以要將eclipse編碼格式統一修改為utf-8(包含jar) mcms 預設jdk1.7+ 由於jdk穩定版本有j
理解JVM(二):垃圾收集演算法
判斷哪些物件需要被回收 引用計數演算法: 給物件中新增一個引用計數器,每當有一個地方引用時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。 但是JV
用DirectX實現魔方(二)
這篇說一下如何構造魔方,主要包括魔方几何體的構造及紋理貼圖。以下論述皆以三階魔方為例,三階魔方共有3 x 3 x 3 = 27個小立方體。 構造魔方 在第一篇裡面說過,最初模型用的是微軟的.x檔案格式,由於魔方要實現按層旋轉,所以不能將整個模型做成一個.x檔案,只能分成若干個小立方體,每個立方體對應一個.
必須知道的八大種排序演算法【java實現】(二) 選擇排序,插入排序,希爾演算法【詳解】
一、選擇排序 1、基本思想:在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然後在剩下的數當中再找最小的與第二個位置的數交換,如此迴圈到倒數第二個數和最後一個數比較為止。 2、例項 3、演算法實現 /** * 選擇排序演算法 * 在未
API 系列教程(二):結合 Laravel 5.5 和 Vue SPA 基於 jwt-auth 實現 API 認證
上一篇我們簡單演示了 Laravel 5.5 中 RESTful API 的構建、認證和測試,本教程將在上一篇教程的基礎上進行昇華。 我們將結合 Laravel 和 Vue 單頁面應用(SPA),在它們的基礎上引入 jwt-auth 實現 API 認證,由於 Laravel 集成了對 Vue
java web 筆記(二):登入認證系統
講完cookie和session(沒看過前一篇部落格的建議先看前一篇),現在簡單討論下登入系統。 簡單的單獨專案登入系統可以做的很簡單,只是用cookie和session就能實現;複雜的登入系統如SSO等可以做的很複雜,需要考慮使用各種認證防資料捕獲等情況。
機器學習與神經網路(二):感知器的介紹和Python程式碼實現
前言:本篇博文主要介紹感知器的相關知識,採用理論+程式碼實踐的方式,進行感知器的學習。本文首先介紹感知器的模型,然後介紹感知器學習規則(Perceptron學習演算法),最後通過Python程式碼實現單層感知器,從而給讀者一個更加直觀的認識。 1.單層感知器模型 單層感知器
【H.264/AVC視訊編解碼技術詳解】十五、H.264的變換編碼(二):H.264整數變換和量化的實現
《H.264/AVC視訊編解碼技術詳解》視訊教程已經在“CSDN學院”上線,視訊中詳述了H.264的背景、標準協議和實現,並通過一個實戰工程的形式對H.264的標準進行解析和實現,歡迎觀看! “紙上得來終覺淺,絕知此事要躬行”,只有自己按照標準文件以程式碼