1. 程式人生 > >用Java實現JVM(二):支援介面、類和物件

用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]); } }

要讓上述程式碼工作,將涉及到了:

  1. 類的初始化

    類靜態成員的初始化,如類成員Main.speaker在何時初始化。

  2. 物件初始化(例項化)

    new Speaker("Hello")如何執行,物件的成員(如private String hello = "";)如何初始化。注意String在JJvm 中被當做 Native 類,那麼 Native 類又如何初始化。

  3. 物件屬性的操作

    包括 Native 類和非 Native 類例項的屬性的操作,如訪問Speaker.hello

  4. 方法呼叫

    包括例項方法、類方法、介面方法的呼叫。

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開啟握手

深入理解JVMJava記憶體區域

執行時資料區域 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_

JVMJVM載入機制

如下圖所示,JVM類載入機制分為五個部分:載入,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。   載入 載入是類載入過程中的一個階段,這個階段會在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口。注意這裡不一

java Restful框架jersey請求對映頁面傳值

jersey的webservice開發基本上都是使用註解,接下來學習常用註解. 一.根資源類 [email protected]註解 @Path("/hello") public class HelloWorldController { @G

基於MCMSJava開發網站

上篇說到,將下載下來的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的標準進行解析和實現,歡迎觀看! “紙上得來終覺淺,絕知此事要躬行”,只有自己按照標準文件以程式碼