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]);
}
}
要让上述代码工作,将涉及到了:
-
类的初始化
类静态成员的初始化,如类成员
Main.speaker
在何时初始化。 -
对象初始化(实例化)
如
new Speaker("Hello")
如何执行,对象的成员(如private String hello = "";
)如何初始化。注意String
在JJvm 中被当做 Native 类,那么 Native 类又如何初始化。 -
对象属性的操作
包括 Native 类和非 Native 类实例的属性的操作,如访问
Speaker.hello
。 -
方法调用
包括实例方法、类方法、接口方法的调用。
2. 抽象
为了支持类和对象的概念,我在 JVM 层做了抽象,如下图:
我定义了类和对象的基本形态(这里只列出了接口的主要方法):
-
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. 类的初始化
类的初始化即调用类的
方法, 如下面是示例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."":(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<>("", "()V"));
if(method != null){
method.call(env, null);
}
}
}
也就是找到
方法,然后按正常方法的形式执行。关于类的初始化方法何时被执行,这里摘录了《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."":(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
该指令用于调用超类构造方法、实例初始化方法或者私有方法。此处调用的是构造方法
。/** * 调用超类构造方法、实例初始化方法或者私有方法。 */ 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
再看Speaker
构造函数
的字节码:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()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
4. 结束
使用新的 JJvm 执行文章开始处的示例,将得到以下输出:
> org/caoym/samples/sample2/Main.@0:NEW
> org/caoym/samples/sample2/Main.@1:DUP
> org/caoym/samples/sample2/Main.@2:LDC
> org/caoym/samples/sample2/Main.@3:INVOKESPECIAL
> org/caoym/samples/sample2/Speaker.@0:ALOAD_0
> org/caoym/samples/sample2/Speaker.@1:INVOKESPECIAL
> org/caoym/samples/sample2/Speaker.@2:ALOAD_0
> org/caoym/samples/sample2/Speaker.@3:LDC
> org/caoym/samples/sample2/Speaker.@4:PUTFIELD
> org/caoym/samples/sample2/Speaker.@5:ALOAD_0
> org/caoym/samples/sample2/Speaker.@6:ALOAD_1
> org/caoym/samples/sample2/Speaker.@7:PUTFIELD
> org/caoym/samples/sample2/Speaker.@8:RETURN
> org/caoym/samples/sample2/Main.@4:PUTSTATIC
> org/caoym/samples/sample2/Main.@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 亦可!<<。