没错这又是一篇介绍 JVM 的文章,这类文章网上已经很多,不同角度、不同深度、不同广度,也都不乏优秀的。为什么还要来一篇?首先对于我来说,我正在学习 Java,了解 JVM 的实现对学习 Java 当然很有必要,但我已经做了多年 C++开发,就算我用 C++实现一个 JVM,我还是个 C++码农,而用 Java 实现,即能学习 Java 语法,又能理解 JVM,一举两得。其次,作为读者,hotspot 或者其他成熟 JVM 实现的源码读起来并不轻松,特别是对没有 C/C++经验的人来说,如果只是想快速了解 JVM 的工作原理,并且希望运行和调试一下 JVM 的代码来加深理解,那么这篇文章可能更合适。
我将用 Java 实现一个 JAVA 虚拟机(源码在这下载,加 Star 亦可),一开始它会非常简单,实际上简单得只够运行 HelloWorld。虽然简单,但是我尽量让其符合 JVM 标准,目前主要参考依据是《 Java 虚拟机规范 ( Java SE 7 中文版)》。
先写一个 HelloWorld,代码如下:
package org.caoym;
public class HelloWorld {
public static void main(String[] args){
System.out.println("Hello World");
}
}
我期望所实现的虚拟机(姑且命名为 JJvm 吧),可以通过以下命令运行:
$ java org.caoym.jjvm.JJvm org.caoym.HelloWorld
Hello World
接下来我们开始实现 JJvm,下面是其入口代码,后面将逐步介绍:
public void run(String[] args) throws Exception {
Env env = new Env(this);
//加载初始类
JvmClass clazz = findClass(initialClass);
//找到入口方法
JvmMethod method = clazz.getMethod(
"main",
"([Ljava/lang/String;)V",
(int)(AccessFlags.JVM_ACC_STATIC|AccessFlags.JVM_ACC_PUBLIC));
//执行入口方法
method.call(env, clazz, (Object[]) args);
}
我们将包含 main 入口的类称为初始类,JJvm 首先需要根据org.caoym.HelloWorld
类名,找到.class 文件,然后加载并解析、校验字节码,这些步骤正是 ClassLoader (类加载器)做的事情。HelloWorld.class
内容大致如下:
cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 164c 6f72 672f 6361
6f79 6d2f 4865 6c6c 6f57 6f72 6c64 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
...
没错是紧凑的二进制格式,需要按规范解析,不过我并不打算自己写解析程序,可以直接用com.sun.tools.classfile.ClassFile
,这也是用 JAVA 写好处。下面是HelloWorld.class
解析后的内容(通过javap -v HelloWorld.class
输出):
public class org.caoym.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // org/caoym/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/caoym/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 org/caoym/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public org.caoym.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/caoym/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
可以看到 HelloWorld.class 文件中主要包含几部分:
常量池( Constant pool )
常量池中记录了当前类中用到的常量,包括方法名、类名、字符串常量等,如:#3 = String #23
, #3
为此常量的索引,字节码执行时通过此索引获取此常量,String
为常量类型, 还可以是 Methodref (方法引用)、Fieldref(属性引用)等。
方法定义
此处定义了方法的访问方式(如 PUBLIC、STATIC)、字节码等,关于字节码的执行方式将在后面介绍。
以下为类加载器的部分代码实现:
/**
* 虚拟机的引导类加载器
*/
public class JvmClassLoader {
// ... 此处省略部分代码
public JvmClass loadClass(String className) throws ClassNotFoundException{
String fileName = classPath + "/"+className.replace(".", "/")+".class";
Path path = Paths.get(fileName);
//如果文件存在,加载文件字节码
//否则尝试通过虚拟机宿主加载指定类,并将加载后的类当做 native 类
if(Files.exists(path)){
return JvmOpcodeClass.read(path);
}else{
return new JvmNativeClass(Class.forName(className.replace("/",".")));
}
}
}
类加载器可以加载两种形式的类:JvmOpcodeClass
和 JvmNativeClass
,均继承自JvmClass
。其中 JvmOpcodeClass 表示用户定义的类,通过字节码执行,也就是这个例子中的HelloWorld
;JvmNativeClass
表示 JVM 提供的原生类,可直接调用原生类执行,比如 java.lang.System
。这里把所有非项目内的类,都当做原始类处理,以便简化虚拟机的实现。
JVM 规定入口是static public void main(String[])
,为了能够查找指定类的方法,JvmOpcodeClass
和JvmNativeClass
都需要提供getMethod
方法, 当然 main 方法肯定存在JvmOpcodeClass
中:
public class JvmOpcodeClass implements JvmClass{
private JvmOpcodeClass(ClassFile classFile) throws ConstantPoolException {
this.classFile = classFile;
for (Method method : classFile.methods) {
String name = method.getName(classFile.constant_pool);
String desc = method.descriptor.getValue(classFile.constant_pool);
methods.put(name+":"+desc, new JvmOpcodeMethod(classFile, method));
}
}
@Override
public JvmMethod getMethod(String name, String desc, int flags) throws NoSuchMethodException {
JvmOpcodeMethod method = methods.get(name+":"+desc);
//... check method != null
return method;
}
}
下图为以HelloWorld
的main()
方法的执行过程:
下面将详细说明。
每一个虚拟机线程都有自己私有的虚拟机栈(Java Virtual Machine Stack),用于存储栈帧。每一次方法调用,即产生一个新的栈帧,并推入栈顶,函数返回后,此栈帧从栈顶推出。以下为 JJvm 中虚拟机栈的部分代码:
public class Stack {
//创建新栈并推入栈顶,用于 native 方法调用
public StackFrame newFrame() {
StackFrame frame = new StackFrame(null, null, 0, 0);
frames.push(frame, 1);
return frame;
}
//创建新栈并推入栈顶,用于 opcode 方法调用
public StackFrame newFrame(ConstantPool constantPool,
Opcode[] opcodes,
int variables,
int stackSize) {
StackFrame frame = new StackFrame(constantPool, opcodes, variables, stackSize);
frames.push(frame, 1);
return frame;
}
public StackFrame currentFrame(){...} //获取当前正在执行的栈帧
public StackFrame popFrame(){...} //从栈顶退出一个栈帧
}
栈帧用于保存当前函数调用的上下文信息,以下为 JJvm 中栈帧的部分代码:
public class StackFrame {
private int pc=0; //程序计数器
public StackFrame(ConstantPool constantPool,
Opcode[] opcodes,
int variables,
int stackSize) {
this.constantPool = constantPool; //常量池
this.opcodes = opcodes; //当前方法的字节码
this.operandStack = new SlotsStack(stackSize); //操作数栈
this.localVariables = new Slots(variables); //局部变量表
}
public Slots<Object> getLocalVariables() {...} //局部变量表
public SlotsStack<Object> getOperandStack() {...} //操作数栈
public ConstantPool getConstantPool() {...} //常量池
public void setPC(int pc) {...} //设置程序计数器
//设置方法返回值,一旦设置,此帧需要被退出栈顶,并将返回值推入上一个栈帧的操作数栈
public void setReturn(Object returnVal, String returnType) {...}
public Object getReturn() {...} //获取当前方法返回值
public String getReturnType() {...} //获取当前方法返回值类型
public boolean isReturned() {...} //判断当前方法是否已经返回
public int getPC() {...} //获取程序计数器
public int increasePC() {...} //递增程序计数器
public Opcode[] getOpcodes() {...} //当前方法的字节码
}
说明:
局部变量表
保存当前方法的局部变量、实例的 this 指针和方法的实参。函数执行过程中,部分字节码会操作或读取局部变量表。局部变量表的长度由编译期决定。
常量池
引用当前类的常量池。
字节码内容
以数组形式保存的当期方法的字节码。
程序计数器
记录当前真在执行的字节码的位置。
操作数栈
操作数栈用来准备字节码调用时的参数并接收其返回结果,操作数栈的长度由编译期决定。
方法调用的过程大致如下:
以下为 JJvm 中的部分代码:
public class JvmOpcodeMethod implements JvmMethod {
public void call(Env env, Object thiz, Object ...args) throws Exception {
// 每次方法调用都产生一个新的栈帧,当前方法返回后,将其栈帧设置为已返回,BytecodeInterpreter.run 会在检查到返回后,将栈帧推
// 出栈,并将返回值(如果有)推入上一个栈帧的操作数栈
StackFrame frame = env.getStack().newFrame(
classFile.constant_pool,
opcodes,
codeAttribute.max_locals,
codeAttribute.max_stack);
// Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的 参数将会传递至从 0 开始的连续的局部变量表位置
// 上。特别地,当一个实例方法被调用的时候, 第 0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的“ this ”
// 关键字)。后续的其他参数将会传递至从 1 开始的连续的局部变量表位置上。
Slots<Object> locals = frame.getLocalVariables();
int pos = 0;
if(!method.access_flags.is(AccessFlags.ACC_STATIC)){
locals.set(0, thiz, 1);
pos++;
}
for (Object arg : args) {
locals.set(pos++, arg, 1);
}
//解释执行字节码
BytecodeInterpreter.run(env);
}
}
字节码的执行过程如下:
return
等,则设置栈帧为已返回状态。invokevirtual
等嵌套调用其他方法,则创建新的栈帧,并回到第一步。以下为 JJvm 中解释执行字节码的部分代码:
public class BytecodeInterpreter {
//执行字节码
public static void run(Env env) throws Exception {
//只需要最外层调用执行栈上操作
if(env.getStack().isRunning()) return;
StackFrame frame;
Stack stack = env.getStack();
stack.setRunning(true);
while ((frame = stack.currentFrame()) != null){
//如果栈帧被设置为返回,则将其返回值推入上一个栈帧的操作数栈
if(frame.isReturned()){
stack.popFrame();
//如果有返回值,则将返回值推入上一个栈帧的操作数栈。
if(!"void".equals(frame.getReturnType())){
frame = stack.currentFrame();
if(frame != null){
frame.getOperandStack().push(frame.getReturn());
}
}
continue;
}
Opcode[] codes = frame.getOpcodes();
int pc = frame.increasePC();
codes[pc].call(env, frame);
}
}
// opcode 的实现
static {
//return: 从当前方法返回 void。
OPCODES[Constants.RETURN] = (Env env, StackFrame frame, byte[] operands)->{
frame.setReturn(null, "void");
};
//getstatic: 获取对象的静态字段值
OPCODES[Constants.GETSTATIC] = (Env env, StackFrame frame, byte[] operands)->{
int arg = (operands[0]<<4)|operands[1];
ConstantPool.CONSTANT_Fieldref_info info
= (ConstantPool.CONSTANT_Fieldref_info)frame.getConstantPool().get(arg);
//静态字段所在的类
JvmClass clazz = env.getVm().findClass(info.getClassName());
//静态字段的值
Object value = clazz.getField(
info.getNameAndTypeInfo().getName(),
info.getNameAndTypeInfo().getType(),
AccessFlags.ACC_STATIC
);
frame.getOperandStack().push(value, 1);
};
//ldc: 将 int,float 或 String 型常量值从常量池中推送至栈顶
OPCODES[Constants.LDC] = (Env env, StackFrame frame, byte[] operands)->{
int arg = operands[0];
ConstantPool.CPInfo info = frame.getConstantPool().get(arg);
frame.getOperandStack().push(asObject(info), 1);
};
//invokevirtual: 调用实例方法
OPCODES[Constants.INVOKEVIRTUAL] = (Env env, StackFrame frame, byte[] operands)->{
int arg = (operands[0]<<4)|operands[1];
ConstantPool.CONSTANT_Methodref_info info
= (ConstantPool.CONSTANT_Methodref_info)frame.getConstantPool().get(arg);
String className = info.getClassName();
String name = info.getNameAndTypeInfo().getName();
String type = info.getNameAndTypeInfo().getType();
JvmClass clazz = env.getVm().findClass(className);
JvmMethod method = clazz.getMethod(name, type, 0);
//从操作数栈中推出方法的参数
Object args[] = frame.getOperandStack().dumpAll();
method.call(env, args[0], Arrays.copyOfRange(args,1, args.length));
};
// ... 以下省略
}
}
Native 方法的调用要更简单一些,只需调用已存在的实现即可,代码如下:
public class JvmNativeMethod implements JvmMethod {
private Method method;
@Override
public void call(Env env, Object thiz, Object... args) throws Exception {
StackFrame frame = env.getStack().newFrame();
Object res = method.invoke(thiz, args);
//将返回值推入调用者的操作数栈
frame.setReturn(res, method.getReturnType().getName());
}
}
当目前为止,我们的“刚好够运行 HelloWorld ”的 JVM 已经完成,完整代码可在这里下载。当然这个 JVM 并不完整,缺少很多内容,如类和实例的初始化、多线程问题、反射、GC 等等。我争取逐步完善 JJvm,并奉上更多文章。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.