用Java实现JVM(二):支持接口、类和对象

1. 概述

接上篇《用Java实现JVM(一):刚好够运行 HelloWorld》

>>源码在这下载,加 Star 亦可!<<

我的 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/Main.main@0:GETSTATIC
> org/caoym/samples/sample2/Main.main@1:ALOAD_0
> org/caoym/samples/sample2/Main.main@2:ICONST_0
> org/caoym/samples/sample2/Main.main@3:AALOAD
> org/caoym/samples/sample2/Main.main@4:INVOKEINTERFACE
> org/caoym/samples/sample2/Speaker.helloTo@0:GETSTATIC
> org/caoym/samples/sample2/Speaker.helloTo@1:NEW
> org/caoym/samples/sample2/Speaker.helloTo@2:DUP
> org/caoym/samples/sample2/Speaker.helloTo@3:INVOKESPECIAL
> org/caoym/samples/sample2/Speaker.helloTo@4:ALOAD_0
> org/caoym/samples/sample2/Speaker.helloTo@5:GETFIELD
> org/caoym/samples/sample2/Speaker.helloTo@6:INVOKEVIRTUAL
> org/caoym/samples/sample2/Speaker.helloTo@7:LDC
> org/caoym/samples/sample2/Speaker.helloTo@8:INVOKEVIRTUAL
> org/caoym/samples/sample2/Speaker.helloTo@9:ALOAD_1
> org/caoym/samples/sample2/Speaker.helloTo@10:INVOKEVIRTUAL
> org/caoym/samples/sample2/Speaker.helloTo@11:INVOKEVIRTUAL
> org/caoym/samples/sample2/Speaker.helloTo@12:INVOKEVIRTUAL
Hello World
> org/caoym/samples/sample2/Speaker.helloTo@13:RETURN
> org/caoym/samples/sample2/Main.main@5:RETURN

符号“>”开始的行是运行日志,日志记录了指令的执行步骤。

>>源码在这下载,加 Star 亦可!<<

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容