1. 前言
沒錯(cuò)這又是一篇介紹 JVM 的文章,這類文章網(wǎng)上已經(jīng)很多叛拷,不同角度舌厨、不同深度、不同廣度忿薇,也都不乏優(yōu)秀的裙椭。為什么還要來一篇?首先對于我來說煌恢,我正在學(xué)習(xí) Java骇陈,了解JVM的實(shí)現(xiàn)對學(xué)習(xí)Java當(dāng)然很有必要,但我已經(jīng)做了多年C++開發(fā)瑰抵,就算我用C++實(shí)現(xiàn)一個(gè)JVM你雌,我還是個(gè)C++碼農(nóng),而用 Java實(shí)現(xiàn)二汛,即能學(xué)習(xí) Java 語法婿崭,又能理解 JVM,一舉兩得肴颊。其次氓栈,作為讀者,hotspot或者其他成熟JVM實(shí)現(xiàn)的源碼讀起來并不輕松婿着,特別是對沒有C/C++經(jīng)驗(yàn)的人來說授瘦,如果只是想快速了解JVM的工作原理醋界,并且希望運(yùn)行和調(diào)試一下JVM的代碼來加深理解,那么這篇文章可能更合適提完。
我將用Java實(shí)現(xiàn)一個(gè)JAVA虛擬機(jī)(源碼在這下載形纺,加 Star 亦可),一開始它會(huì)非常簡單徒欣,實(shí)際上簡單得只夠運(yùn)行HelloWorld逐样。雖然簡單,但是我盡量讓其符合 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吧)粗梭,可以通過以下命令運(yùn)行:
$ java org.caoym.jjvm.JJvm org.caoym.HelloWorld
Hello World
接下來我們開始實(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
...
沒錯(cuò)是緊湊的二進(jìn)制格式,需要按規(guī)范解析蹦锋,不過我并不打算自己寫解析程序炕置,可以直接用com.sun.tools.classfile.ClassFile
荣挨,這也是用JAVA寫好處。下面是HelloWorld.class
解析后的內(nèi)容(通過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í)通過此索引獲取此常量,String
為常量類型, 還可以是Methodref (方法引用)介杆、Fieldref(屬性引用)等鹃操。 -
方法定義
此處定義了方法的訪問方式(如 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é)碼
//否則嘗試通過虛擬機(jī)宿主加載指定類椰拒,并將加載后的類當(dāng)做 native 類
if(Files.exists(path)){
return JvmOpcodeClass.read(path);
}else{
return new JvmNativeClass(Class.forName(className.replace("/",".")));
}
}
}
類加載器可以加載兩種形式的類:JvmOpcodeClass
和 JvmNativeClass
晶渠,均繼承自JvmClass
。其中JvmOpcodeClass 表示用戶定義的類燃观,通過字節(jié)碼執(zhí)行乱陡,也就是這個(gè)例子中的HelloWorld
;JvmNativeClass
表示JVM 提供的原生類仪壮,可直接調(diào)用原生類執(zhí)行憨颠,比如 java.lang.System
。這里把所有非項(xiàng)目內(nèi)的類积锅,都當(dāng)做原始類處理爽彤,以便簡化虛擬機(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í)行過程:
下面將詳細(xì)說明。
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é)碼
}
說明:
-
局部變量表
保存當(dāng)前方法的局部變量勤家、實(shí)例的this指針和方法的實(shí)參。函數(shù)執(zhí)行過程中柳恐,部分字節(jié)碼會(huì)操作或讀取局部變量表伐脖。局部變量表的長度由編譯期決定热幔。
-
常量池
引用當(dāng)前類的常量池。
-
字節(jié)碼內(nèi)容
以數(shù)組形式保存的當(dāng)期方法的字節(jié)碼讼庇。
-
程序計(jì)數(shù)器
記錄當(dāng)前真在執(zhí)行的字節(jié)碼的位置绎巨。
-
操作數(shù)棧
操作數(shù)棧用來準(zhǔn)備字節(jié)碼調(diào)用時(shí)的參數(shù)并接收其返回結(jié)果,操作數(shù)棧的長度由編譯期決定蠕啄。
5.3. 方法調(diào)用
方法調(diào)用的過程大致如下:
- 新建棧幀场勤,并推入虛擬機(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ī)使用局部變量表來完成方法調(diào)用時(shí)的參數(shù)傳遞她倘,當(dāng)一個(gè)方法被調(diào)用的時(shí)候,它的 參數(shù)將會(huì)傳遞至從 0 開始的連續(xù)的局部變量表位置
// 上作箍。特別地硬梁,當(dāng)一個(gè)實(shí)例方法被調(diào)用的時(shí)候, 第 0 個(gè)局部變量一定是用來存儲(chǔ)被調(diào)用的實(shí)例方法所在的對象的引用(即 Java 語言中的“this”
// 關(guān)鍵字)蒙揣。后續(xù)的其他參數(shù)將會(huì)傳遞至從 1 開始的連續(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í)行過程如下:
- 獲取棧頂?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,多謝 @樂浩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: 獲取對象的靜態(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)用要更簡單一些熊响,只需調(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í)例的初始化、多線程問題奥喻、反射偶宫、GC 等等。我爭取逐步完善JJvm环鲤,并奉上更多文章纯趋。
下一篇:用Java實(shí)現(xiàn)JVM(二):支持接口、類和對象