用 Java 实现 JVM(二):支持接口、类和对象 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
caoyangmin
V2EX    Java

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

  •  3
     
  •   caoyangmin
    caoym 2017-10-11 08:59:19 +08:00 3636 次点击
    这是一个创建于 3005 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 亦可!<<

    3 条回复    2017-10-11 13:07:31 +08:00
    KeepPro
        1
    KeepPro  
       2017-10-11 09:09:49 +08:00 via Android
    太长我就不看了。写东西切记不要写出流水账,并且要让别人知道你在说什么。
    而且据说 v2 崇尚简介,直接贴你的链接,然后用一句话总结一下就好。感兴趣的自会去看,也节省了不感兴趣的人的时间和流量。
    ofblyt
        2
    ofblyt  
       2017-10-11 10:33:14 +08:00
    刚好这几天在看《自己动手写 java 虚拟机》,跟楼主写的内容差不多,不过是用 golang 实现的,推荐感兴趣的同学看一下
    hantsy
        3
    hantsy  
       2017-10-11 13:07:31 +08:00
    @ofblyt NB
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     868 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 19:40 PVG 03:40 LAX 11:40 JFK 14:40
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86