
1. 前言
沒(méi)錯(cuò)這又是一篇介紹 JVM 的文章,這類文章網(wǎng)上已經(jīng)很多,不同角度、不同深度、不同廣度,也都不乏優(yōu)秀的。為什么還要來(lái)一篇?首先對(duì)于我來(lái)說(shuō),我正在學(xué)習(xí) Java,了解JVM的實(shí)現(xiàn)對(duì)學(xué)習(xí)Java當(dāng)然很有必要,但我已經(jīng)做了多年C++開(kāi)發(fā),就算我用C++實(shí)現(xiàn)一個(gè)JVM,我還是個(gè)C++碼農(nóng),而用 Java實(shí)現(xiàn),即能學(xué)習(xí) Java 語(yǔ)法,又能理解 JVM,一舉兩得。其次,作為讀者,hotspot或者其他成熟JVM實(shí)現(xiàn)的源碼讀起來(lái)并不輕松,特別是對(duì)沒(méi)有C/C++經(jīng)驗(yàn)的人來(lái)說(shuō),如果只是想快速了解JVM的工作原理,并且希望運(yùn)行和調(diào)試一下JVM的代碼來(lái)加深理解,那么這篇文章可能更合適。
我將用Java實(shí)現(xiàn)一個(gè)JAVA虛擬機(jī)(源碼在這下載,加 Star 亦可),一開(kāi)始它會(huì)非常簡(jiǎn)單,實(shí)際上簡(jiǎn)單得只夠運(yùn)行HelloWorld。雖然簡(jiǎn)單,但是我盡量讓其符合 JVM 標(biāo)準(zhǔn),目前主要參考依據(jù)是《Java虛擬機(jī)規(guī)范 (Java SE 7 中文版)》。
2. 準(zhǔn)備
先寫一個(gè)HelloWorld,代碼如下:
package org.caoym;
public class HelloWorld {
public static void main(String[] args){
System.out.println("Hello World");
}
}
我期望所實(shí)現(xiàn)的虛擬機(jī)(姑且命名為JJvm吧),可以通過(guò)以下命令運(yùn)行:
$ java org.caoym.jjvm.JJvm org.caoym.HelloWorld
Hello World
接下來(lái)我們開(kāi)始實(shí)現(xiàn)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));
//執(zhí)行入口方法
method.call(env, clazz, (Object[]) args);
}
3. 加載初始類
我們將包含 main 入口的類稱為初始類,JJvm 首先需要根據(jù)org.caoym.HelloWorld類名,找到.class 文件,然后加載并解析、校驗(yàn)字節(jié)碼,這些步驟正是 ClassLoader(類加載器)做的事情。HelloWorld.class內(nèi)容大致如下:
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
...
沒(méi)錯(cuò)是緊湊的二進(jìn)制格式,需要按規(guī)范解析,不過(guò)我并不打算自己寫解析程序,可以直接用com.sun.tools.classfile.ClassFile,這也是用JAVA寫好處。下面是HelloWorld.class解析后的內(nèi)容(通過(guò)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)
常量池中記錄了當(dāng)前類中用到的常量,包括方法名、類名、字符串常量等,如:
#3 = String #23,#3為此常量的索引,字節(jié)碼執(zhí)行時(shí)通過(guò)此索引獲取此常量,String為常量類型, 還可以是Methodref (方法引用)、Fieldref(屬性引用)等。 -
方法定義
此處定義了方法的訪問(wèn)方式(如 PUBLIC、STATIC)、字節(jié)碼等,關(guān)于字節(jié)碼的執(zhí)行方式將在后面介紹。
以下為類加載器的部分代碼實(shí)現(xiàn):
/**
* 虛擬機(jī)的引導(dǎo)類加載器
*/
public class JvmClassLoader {
// ... 此處省略部分代碼
public JvmClass loadClass(String className) throws ClassNotFoundException{
String fileName = classPath + "/"+className.replace(".", "/")+".class";
Path path = Paths.get(fileName);
//如果文件存在,加載文件字節(jié)碼
//否則嘗試通過(guò)虛擬機(jī)宿主加載指定類,并將加載后的類當(dāng)做 native 類
if(Files.exists(path)){
return JvmOpcodeClass.read(path);
}else{
return new JvmNativeClass(Class.forName(className.replace("/",".")));
}
}
}
類加載器可以加載兩種形式的類:JvmOpcodeClass和 JvmNativeClass,均繼承自JvmClass。其中JvmOpcodeClass 表示用戶定義的類,通過(guò)字節(jié)碼執(zhí)行,也就是這個(gè)例子中的HelloWorld;JvmNativeClass表示JVM 提供的原生類,可直接調(diào)用原生類執(zhí)行,比如 java.lang.System。這里把所有非項(xiàng)目?jī)?nèi)的類,都當(dāng)做原始類處理,以便簡(jiǎn)化虛擬機(jī)的實(shí)現(xiàn)。
4. 找到入口方法
JVM規(guī)定入口是static public void main(String[]),為了能夠查找指定類的方法,JvmOpcodeClass和JvmNativeClass都需要提供getMethod方法, 當(dāng)然 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;
}
}
5. 執(zhí)行非 Native(字節(jié)碼定義的)方法
下圖為以HelloWorld的main()方法的執(zhí)行過(guò)程:

下面將詳細(xì)說(shuō)明。
5.1. 虛擬機(jī)棧
每一個(gè)虛擬機(jī)線程都有自己私有的虛擬機(jī)棧(Java Virtual Machine Stack),用于存儲(chǔ)棧幀。每一次方法調(diào)用,即產(chǎn)生一個(gè)新的棧幀,并推入棧頂,函數(shù)返回后,此棧幀從棧頂推出。以下為 JJvm中虛擬機(jī)棧的部分代碼:
public class Stack {
//創(chuàng)建新棧并推入棧頂,用于 native 方法調(diào)用
public StackFrame newFrame() {
StackFrame frame = new StackFrame(null, null, 0, 0);
frames.push(frame, 1);
return frame;
}
//創(chuàng)建新棧并推入棧頂,用于 opcode 方法調(diào)用
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(){...} //獲取當(dāng)前正在執(zhí)行的棧幀
public StackFrame popFrame(){...} //從棧頂退出一個(gè)棧幀
}
5.2. 棧幀
棧幀用于保存當(dāng)前函數(shù)調(diào)用的上下文信息,以下為 JJvm 中棧幀的部分代碼:
public class StackFrame {
private int pc=0; //程序計(jì)數(shù)器
public StackFrame(ConstantPool constantPool,
Opcode[] opcodes,
int variables,
int stackSize) {
this.constantPool = constantPool; //常量池
this.opcodes = opcodes; //當(dāng)前方法的字節(jié)碼
this.operandStack = new SlotsStack(stackSize); //操作數(shù)棧
this.localVariables = new Slots(variables); //局部變量表
}
public Slots<Object> getLocalVariables() {...} //局部變量表
public SlotsStack<Object> getOperandStack() {...} //操作數(shù)棧
public ConstantPool getConstantPool() {...} //常量池
public void setPC(int pc) {...} //設(shè)置程序計(jì)數(shù)器
//設(shè)置方法返回值,一旦設(shè)置,此幀需要被退出棧頂,并將返回值推入上一個(gè)棧幀的操作數(shù)棧
public void setReturn(Object returnVal, String returnType) {...}
public Object getReturn() {...} //獲取當(dāng)前方法返回值
public String getReturnType() {...} //獲取當(dāng)前方法返回值類型
public boolean isReturned() {...} //判斷當(dāng)前方法是否已經(jīng)返回
public int getPC() {...} //獲取程序計(jì)數(shù)器
public int increasePC() {...} //遞增程序計(jì)數(shù)器
public Opcode[] getOpcodes() {...} //當(dāng)前方法的字節(jié)碼
}
說(shuō)明:
-
局部變量表
保存當(dāng)前方法的局部變量、實(shí)例的this指針和方法的實(shí)參。函數(shù)執(zhí)行過(guò)程中,部分字節(jié)碼會(huì)操作或讀取局部變量表。局部變量表的長(zhǎng)度由編譯期決定。
-
常量池
引用當(dāng)前類的常量池。
-
字節(jié)碼內(nèi)容
以數(shù)組形式保存的當(dāng)期方法的字節(jié)碼。
-
程序計(jì)數(shù)器
記錄當(dāng)前真在執(zhí)行的字節(jié)碼的位置。
-
操作數(shù)棧
操作數(shù)棧用來(lái)準(zhǔn)備字節(jié)碼調(diào)用時(shí)的參數(shù)并接收其返回結(jié)果,操作數(shù)棧的長(zhǎng)度由編譯期決定。
5.3. 方法調(diào)用
方法調(diào)用的過(guò)程大致如下:
- 新建棧幀,并推入虛擬機(jī)棧。
- 將實(shí)例的this和當(dāng)前方法的實(shí)參設(shè)置到棧幀的局部變量表中。
- 解釋執(zhí)行方法的字節(jié)碼。
以下為 JJvm 中的部分代碼:
public class JvmOpcodeMethod implements JvmMethod {
public void call(Env env, Object thiz, Object ...args) throws Exception {
// 每次方法調(diào)用都產(chǎn)生一個(gè)新的棧幀,當(dāng)前方法返回后,將其棧幀設(shè)置為已返回,BytecodeInterpreter.run會(huì)在檢查到返回后,將棧幀推
// 出棧,并將返回值(如果有)推入上一個(gè)棧幀的操作數(shù)棧
StackFrame frame = env.getStack().newFrame(
classFile.constant_pool,
opcodes,
codeAttribute.max_locals,
codeAttribute.max_stack);
// Java 虛擬機(jī)使用局部變量表來(lái)完成方法調(diào)用時(shí)的參數(shù)傳遞,當(dāng)一個(gè)方法被調(diào)用的時(shí)候,它的 參數(shù)將會(huì)傳遞至從 0 開(kāi)始的連續(xù)的局部變量表位置
// 上。特別地,當(dāng)一個(gè)實(shí)例方法被調(diào)用的時(shí)候, 第 0 個(gè)局部變量一定是用來(lái)存儲(chǔ)被調(diào)用的實(shí)例方法所在的對(duì)象的引用(即 Java 語(yǔ)言中的“this”
// 關(guān)鍵字)。后續(xù)的其他參數(shù)將會(huì)傳遞至從 1 開(kāi)始的連續(xù)的局部變量表位置上。
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);
}
//解釋執(zhí)行字節(jié)碼
BytecodeInterpreter.run(env);
}
}
5.4. 解釋執(zhí)行字節(jié)碼
字節(jié)碼的執(zhí)行過(guò)程如下:
- 獲取棧頂?shù)牡谝粋€(gè)棧幀。
- 獲取當(dāng)前棧的程序計(jì)數(shù)器(PC,其默認(rèn)值為0)指向的字節(jié)碼,程序計(jì)數(shù)器+1。
- 執(zhí)行上一步獲取的字節(jié)碼,推出操作數(shù)棧的元素,作為其參數(shù),執(zhí)行字節(jié)碼。
- 字節(jié)碼返回的值(如果有),重新推入操作數(shù)棧。
- 如果操作數(shù)為
return等,則設(shè)置棧幀為已返回狀態(tài)。 - 如果操作數(shù)為
invokevirtual等嵌套調(diào)用其他方法,則創(chuàng)建新的棧幀,并回到第一步。 - 如果棧幀已設(shè)置為返回,則將返回值推入上一個(gè)棧幀的操作數(shù)棧,并推出當(dāng)前棧。
- 重復(fù)執(zhí)行1~7,直到虛擬機(jī)棧為空。
以下為JJvm中解釋執(zhí)行字節(jié)碼的部分代碼:
public class BytecodeInterpreter {
//執(zhí)行字節(jié)碼
public static void run(Env env) throws Exception {
//只需要最外層調(diào)用執(zhí)行棧上操作
if(env.getStack().isRunning()) return;
StackFrame frame;
Stack stack = env.getStack();
stack.setRunning(true);
while ((frame = stack.currentFrame()) != null){
//如果棧幀被設(shè)置為返回,則將其返回值推入上一個(gè)棧幀的操作數(shù)棧
if(frame.isReturned()){
//原先此處有 bug,多謝 @樂(lè)浩beyond 指出
StackFrame oldFrame = frame;
stack.popFrame();
frame = stack.currentFrame();
//如果有返回值,則將返回值推入上一個(gè)棧幀的操作數(shù)棧。
if(frame != null && !"void".equals(oldFrame.getReturnType())){
frame.getOperandStack().push(oldFrame.getReturn());
}
continue;
}
Opcode[] codes = frame.getOpcodes();
int pc = frame.increasePC();
codes[pc].call(env, frame);
}
}
// opcode 的實(shí)現(xiàn)
static {
//return: 從當(dāng)前方法返回 void。
OPCODES[Constants.RETURN] = (Env env, StackFrame frame, byte[] operands)->{
frame.setReturn(null, "void");
};
//getstatic: 獲取對(duì)象的靜態(tài)字段值
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);
//靜態(tài)字段所在的類
JvmClass clazz = env.getVm().findClass(info.getClassName());
//靜態(tài)字段的值
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: 調(diào)用實(shí)例方法
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);
//從操作數(shù)棧中推出方法的參數(shù)
Object args[] = frame.getOperandStack().dumpAll();
method.call(env, args[0], Arrays.copyOfRange(args,1, args.length));
};
// ... 以下省略
}
}
6. 執(zhí)行 Native 方法
Native方法的調(diào)用要更簡(jiǎn)單一些,只需調(diào)用已存在的實(shí)現(xiàn)即可,代碼如下:
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);
//設(shè)置為已返回
frame.setReturn(res, method.getReturnType().getName());
}
}
7. 結(jié)束
到目前為止,我們的“剛好夠運(yùn)行 HelloWorld”的 JVM 已經(jīng)完成,完整代碼可在這里下載。當(dāng)然這個(gè)JVM 并不完整,缺少很多內(nèi)容,如類和實(shí)例的初始化、多線程問(wèn)題、反射、GC 等等。我爭(zhēng)取逐步完善JJvm,并奉上更多文章。
下一篇:用Java實(shí)現(xiàn)JVM(二):支持接口、類和對(duì)象
