程序员第一个(玩具)JVM
我们都知道Java程序要运行在JVM之上,我们除了面试时会了解下JVM的面试题,之外可能很少会去想JVM是如何工作的。在这篇文章中,我会尝试写一个玩具JVM来展示其背后的核心原理,希望激发你进一步学习的兴趣。
一个简单的目标
package me.kagami.myjvm;
public class Add {
public static int add(int a, int b) {
return a + b;
}
}
首先使用javac Add.java
编译后得到Add.class
文件。该文件是JVM可以执行的二进制文件。接下来要做的事情就是正确地实现一个能够执行Add.class
文件的JVM。
CLASS LOADER
JVM的工作之一是类加载,那么我们需要了解class文件的内容。
如果我们用hexdump插件以16进制视图打开Add.class
文件,我们可以看到class文件的组织形式,但我们现在还完看不懂这个文件。没关系,本文将手把手介绍怎么阅读class文件。
如果查看Java规格说明,那么我们可以学习到classFile的结构。可以看到classFile文件总是以4字节的magic数开头(CAFEBABE)然后是2+2的版本信息 ,以此类推。而cp_info,field_info,method_info,attribute_info会比较复杂,本文具体以cp_info详细说明,只要会看cp_info后,其他三个都一样。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
我们以上文的Add.class
举例
magic[CA FE BA BE] minor_version[00 00] major_version[00 34] constant_pool_count[00 15] 0A 00 03 00 12 07
常量池计数是00 15
,0x15
换成十进制是21,规格说明里有这么一句话:
The
constant_pool
table is indexed from 1 toconstant_pool_count
- 1.
说明常量的个数应该是21-1为20个。那么说明constant_pool_count后面有20个常量信息,那么我们来看看常量池是怎么排列的吧。
根据Java规则说明,cp_info的结构如下:
cp_info {
u1 tag;
u1 info[];
}
忽略info前面的u1,因为规格说明里有这么一句话,说明tag后面是可能有多个字节的:
Each tag byte must be followed by two or more bytes giving information about the specific constant.
我们以上文的Add.class
举例,constant_pool_count后第一个tag是 0A
CA FE BA BE 00 00 00 34 constant_pool_count[00 15] 0A 00 03 00 12 07
根据Java规格说明,我们查看tag表类别
Constant Kind | Tag |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_Dynamic | 17 |
CONSTANT_InvokeDynamic | 18 |
CONSTANT_Module | 19 |
CONSTANT_Package | 20 |
0A
换成十进制是10,所以第一个常量应该是CONSTANT_Methodref类型,那么我们再根据Java规格说明查看CONSTANT_Methodref类型的格式为:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
所以我们继续看Add.class
文件,class_index和name_and_type_index都是常量池的引用,也就是说,class_index指向的是常量池的第3常量,name_and_type_index指向的是第18(0x12为十进制18)常量。
CA FE BA BE 00 00 00 34 constant_pool_count[00 15] CONSTANT_Methodref_info[0A class_index(00 03) name_and_type_index(00 12)] 07
常量池的看法我想你应该能看懂了,那么我现在直接给出常量池的全部解析后的结果,我们直接看第3和第18常量是什么吧。以下是按照tag表分出来的20个常量:
CA FE BA BE 00 00 00 34 constant_pool_count[00 15] [0A 00 03 00 12] [0700 13] [07 00 14] CONSTANT_Utf8_info [01 (00 06) 3C 69 6E 69 74 3E] [01 00 03 28 29 56] [01 00 04 43 6F 64 65] [01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65] [01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65] [01 00 04 74 68 69 73] [01 00 15 4C 6D 65 2F 6B 61 67 61 6D 69 2F 6D 79 6A 76 6D 2F 41 64 64 3B] [01 00 03 61 64 64] [01 00 05 28 49 49 29 49] [01 00 01 61] [01 00 01 49] [01 00 01 62] [01 00 0A 53 6F 75 72 63 65 46 69 6C 65] [01 00 08 41 64 64 2E 6A 61 76 61] [0C 00 04 00 05] [01 00 13 6D 65 2F 6B 61 67 61 6D 69 2F 6D 79 6A 76 6D 2F 41 64 64] [01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74] 00 21 00 02 00 03 00 00 00 00 00 02 00 01 00
....
从中可以看出第3常量是[07 00 14]
其中tag为CONSTANT_Class,其结构为:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
说明name_index也是一个常量的引用,0x14
指向的是第20常量,它是一个CONSTANT_Utf8_info常量,这种常量是utf8表示的字符串,结构是:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
所以第20常量[01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74]
的长度是0x0010
,也就是16个字符,从dump列可以用看出这16个字符是:java/lang/Object
,同理可得第18常量为:0x04
是 ,
0x05
是()V
。
那么第1个常量连起来就是java/lang/Object()V
,但这表示是什么意思呢 ?我们翻阅Java规格说明针对CONSTANT_Methodref_info找到了这么一句话:
If the name of the method in a
CONSTANT_Methodref_info
structure begins with a '