Fork me on GitHub

实现简易JVM

1. 概述

  1. Java 源代码经过编译生成 class 文件
  2. 在不同的操作系统上分别实现 JVM, JVM 在不同操作系统上实现差异很大, 如线程, 图形界面等, 由 JVM 屏蔽与操作系统的接口

1.1 Class 文件格式

1.1.1 魔数 & 版本 & 常量池个数

  • Magic Number
    • 确定这是一个 Java 文件
  • Minor / Major Version: 版本号
  • 16 进制
    • Major Version (0x34) = 52
    • 常量池个数 (0x36) = 54
  • 大端模式(Big-Endian): 高位在前
    • 00 36 而不是 36 00

1.1.2 常量池

0036 代表常量池常量的个数, 后面的 07 通过查表发现含义为 ClassInfotag 值, 而 name_index 值为 2, 代表类名在第二个常量中.

第二个常量开头为 01, 查表得知是一个 Utf8 字符串, 0021 代表长度 length 值为 33. 而后面 33 个字节 63 6F 6D 2F 63 6F 64 65 72 69 73 69 6E 67 2F 65 78 61 6D 70 6C 65 2F 45 6D 70 6C 6F 79 65 65 56 31 转换成字符串之后的值为 com/coderising/example/EmployeeV1

1
2
3
4
CONSTANT_Class_info {
u1 tag; // 值为7
u2 name_index; // 名称索引
}
1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag; // 值为1
u2 length; // 长度
u1 bytes[length]; // 内容
}

1.1.2.1 常量池实例

索引 类型 操作数 1 操作数 2 含义
#1 ClassInfo #2
#2 Utf8 org/destiny/jvm/model/Employee
#3 ClassInfo #4
#4 Utf8 java/lang/Object
#5 Utf8 name
#6 Utf8 Ljava/lang/String
#7 Utf8 age
#8 Utf8 I
#9 Utf8
#10 Utf8
#11 Utf8
#12 MethodRef #3 #13 java.lang.Object<init>()V
#13 NameAndType #9 #14 <init>()V
#14 Utf8 ()V
#15 FieldRef #1 #16 org/destiny/jvm/model/Employee 包含一个 Ljava/lang/String 类型的变量 name
#16 NameAndType #5 #6 Ljava/lang/String 类型的变量 name

1.1.3 访问标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 public 类型
ACC_FINAL 0x0002 声明为 final 类型
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语义
ACC_INTERFACE 0x0200 声明为接口
ACC_ABSTRACT 0x0400 Abstract 类型
ACC_SYNTHETIC 0x1000 这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 注解
ACC_ENUM 0x4000 枚举

1.1.4 类索引, 父类索引

类索引和父类索引都是指向常量池的索引

由于 Java 采用动态连接

动态连接是一个将符号引用解析为直接引用的过程。当java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用

那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务

  1. 查找被引用的类,(如果必要的话就装载它)
  2. 将符号引用替换为直接引用,这样当它以后再次遇到相同的引用时,它就可以立即使用这个直接引用,而不必花时间再次解析这个符号引用了。

1.1.5 接口

1.1.6 字段

1
2
3
4
5
6
7
8
9
u2 fields_count;         // 字段数量

field_info {
u2 access_flags; // 访问控制符
u2 name_index; // 指向常量池的入口
u2 descriptor_index; // 指向常量池的入口
u2 attribute_count; // 该字段的属性数量
attribute_info attributes[attribute_count]; // 属性信息
}

标志字符含义

header 1 header 2
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型的通用前缀, 如 Ljava/lang/Object

1.1.7 方法

1
2
3
4
5
6
7
8
9
u2 methods_count;           // 方法数量

method_info {
u2 access_flags; // 访问标志
u2 name_index; // 指向常量池的入口
u2 descriptor_index; // 指向常量池的入口
u2 attributes_count; // 该字段的属性数量
attribute_info attributes[attributes_count]; // 属性信息
}

(Ljava/lang/String;)V 表示 参数为 String, 返回值为 void 的方法
(Ljava/lang/String;IF)V 表示 参数为 String, int, float, 返回值为 void 的方法

1.1.8 属性

  • 方法和字段都可能有属性
    • 方法中可能有 Code 属性, 字段可能有 Constant Value 属性
  • 属性中可能嵌套属性
    • code 属性中还可能有 Line Number Table, Local Variable Table, Stack Map Table 等属性
  • 虚拟机的实现中还可以自定义属性

1.1.8.1 Constant Value

如果某字段为静态类型(access_flag 中包含 ACC_STATIC 标志)
将会被分配 Constant Value 属性
1
2
3
4
5
ConstantValue_attribute {
u2 attribute_name_index; // 必须是对常量池的一个有效索引, 常量池在该索引处的项必须是 UTF8Info, 表示字符串 "ConstantValue"
u4 attribute_length; // 固定为 2
u2 constantvalue_index; // 必须是对常量池的一个有效索引, 常量池在该索引处的项给出该属性表示的常量值, 可能的值有 Constant_String, Constant_Long 等
}

1.1.8.2 Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Code_attribute {
u2 attribute_name_index; // 指向常量池, 应该是 UTF8Info, 且值为 "Code"
u4 attribute_length; // 属性长度, 不包括开始的 6 个字节
u2 max_stack; // 操作数栈的最大深度
u2 max_locals; // 最大局部变量表个数
u4 code_length; // 该方法的代码长度
u1 code[code_length]; // 真正的字节码
u2 exception_table_length; // 捕获异常表的长度
{
u2 start_pc; // 捕获起始地址
u2 end_pc; // 捕获结束地址
u2 handler_pc; //
u2 catch_type; // 异常类型
} exception_table[exception_table_length]; // 捕获异常表
u2 attributes_count; //
attribute_info attributes[attributes_count];
}

Code 属性中的字节码

字节码 命令 含义
2A aload_0 从局部变量表第 0 个值压入操作数栈
B4 00 15 getfield #21 获取对象的字段值
10 1E bipush 30 将 30 压入栈中
A2 00 0E if_icmp_ge 20 将当前

1.1.8.3 LineNumber

code属性的一个子属性
可选的变长属性, 维护 Java 源代码行号与字节码行号(偏移量之间的对应关系)
1
2
3
4
5
6
7
8
9
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc; // 字节码偏移量
u2 line_number; // 行号
} line_number_table[line_number_table_length];
}

1.1.8.4 LocalVariableTable 属性

code属性的一个子属性
可选的变长属性, 维护栈帧中局部变量表中变量与 Java 源码中定义变量的关系
1
2
3
4
5
6
7
8
9
10
11
12
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc; // 局部变量位于 [start_pc, start_pc + length)之间
u2 length;
u2 name_index; // 局部变量的名称索引
u2 descriptor_index; // 局部变量的描述符索引
u2 index; // 局部变量在栈帧中的索引
} local_variable_table[local_variable_table_length];
}

1.2 JVM 运行时动态行为

  • 线程中包含函数栈帧, 其中每个函数帧表示某一个函数的调用过程
  • 在每一个函数帧的内部, JVM 又细分了 局部变量表, 操作数栈
  • 局部变量和操作数栈中的变量会引用堆中的对象
  • 常量池引用指向方法区, 方法区保存了类的元数据以及方法的字节码

1.2.1 实例

Java 源码:

1
2
3
4
5
6
7
8
9
public class Test {
void add(int i, int j) {
int num = i + j;
}

void demo() {
add(10, 20);
}
}

转换成字节码后:

1
2
3
4
5
6
7
8
9
10
11
12
13
demo:
0: aload_0
1: bipush 10
3: bipush 20
5: invokevirtial #2
8: return

add:
0: aload_1
1: aload_2
2: iadd
3: istore_3
4: return

调用 add 函数, 生成新的函数帧

0: aload_1: 将局部变量表第 1 个变量压入操作数栈;
1: aload_2: 将局部变量表第 2 个变量压入操作数栈;
2: iadd: 将操作数栈顶端的两个元素弹出, 相加并将结果压入栈顶
3: istore_3: 将操作数栈栈顶元素放在局部变量表第 3 个元素中
4: return: 执行完毕

2. ClassLoader

2.1 Java 是动态链接

  • C: 编译 -> 链接 -> 生成 .exe -> 执行
    • 函数 A 调用函数 B, 在链接时会直接在函数 A 中记录函数 B 的地址
  • Java: 编译 -> .class -> 装载执行
    • 类 A 中使用了另一个类 B, 在 A.class 中只保存类 B 的名称, 而不会保留 B 的 “地址”
    • 在运行时根据名称来查找类, 装载类

2.2 类加载器的委托模型

工作原理

2.3 类加载器的命名空间

类加载器 + 类名 唯一确定一个类, 只有同一个加载器加载的类才是相同的类.

2.4 验证

2.5 自定义类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MyClassLoader extends ClassLoader {

private List<String> classPaths = new LinkedList<>();

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] byteCodes = loadByteCode(name);
if (byteCodes == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, byteCodes, 0, byteCodes.length);
}
}

private byte[] loadClassFile(String classFileName) {
for (String classPath: classPaths) {
String realPath = classPath + File.separatorChar + classFileName.replace('.', File.separatorChar) + ".class";

File file = new File(classFileName);
if (file.exists()) {
try {
return IOUtils.toByteArray(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}

DefineClass 方法

  • protected final Class<?> defineClass(String name, byte[] b, int off, int length) throws ClassFormatError;
  • 只要传递给该方法一个合法字节数组, 就可以转化成一个 Class 对象, 这就意味着可以从任何地方组装类:
    • 磁盘
    • zip 文件
    • 网络
    • 运行时动态生成

3. 常量池

3.1 常见结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
CONSTANT_Class_info {
u1 tag; // 7
u2 name_index;
}

CONSTANT_Utf8_info {
u1 tag; // 1
u2 length; // 长度
u1 bytes[length]; // content
}

CONSTANT_String_info {
u1 tag; //
u2 string_index;
}

CONSTANT_Fieldref_info {
u1 tag; // 9
u2 class_index; //
u2 name_and_type_index;
}

CONSTANT_Methodref_info {
u1 tag; // 10
u2 class_index;
u2 name_and_type_index;
}

CONSTANT_NameAndType_info {
u1 tag; // 12
u2 class_index;
u2 descriptor_index;
}

3.2 访问标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 public 类型
ACC_FINAL 0x0002 声明为 final 类型
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语义
ACC_INTERFACE 0x0200 声明为接口
ACC_ABSTRACT 0x0400 Abstract 类型
ACC_SYNTHETIC 0x1000 这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 注解
ACC_ENUM 0x4000 枚举

4. 字段 & 方法

4.1 字段

1
2
3
4
5
6
7
8
9
u2 fields_count;         // 字段数量

field_info {
u2 access_flags; // 访问控制符
u2 name_index; // 指向常量池的入口
u2 descriptor_index; // 指向常量池的入口
u2 attribute_count; // 该字段的属性数量
attribute_info attributes[attribute_count]; // 属性信息
}

可以看到上图中有两个字段, 分别为 String 类型的 name, 和 int 类型的 age

  • Name Index 表示常量池中变量名称的索引
  • Desc Index 表示常量池中变量类型的索引

4.2 方法

1
2
3
4
5
6
7
8
9
u2 methods_count;           // 方法数量

method_info {
u2 access_flags; // 访问标志
u2 name_index; // 指向常量池的入口
u2 descriptor_index; // 指向常量池的入口
u2 attributes_count; // 该字段的属性数量
attribute_info attributes[attributes_count]; // 属性信息
}

以第一个方法为例:

  • Name Index 表示方法名称为 <init>, 即构造方法
  • Desc Index 表示方法签名为 (Ljava/lang/String;I)V, 即 (String, int):void

4.3 属性

4.3.1 Code 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Code_attribute {
u2 attribute_name_index; // 指向常量池, 应该是 UTF8Info, 且值为 "Code"
u4 attribute_length; // 属性长度, 不包括开始的 6 个字节
u2 max_stack; // 操作数栈的最大深度
u2 max_locals; // 最大局部变量表个数
u4 code_length; // 该方法的代码长度
u1 code[code_length]; // 真正的字节码
u2 exception_table_length; // 捕获异常表的长度
{
u2 start_pc; // 捕获起始地址
u2 end_pc; // 捕获结束地址
u2 handler_pc; //
u2 catch_type; // 异常类型
} exception_table[exception_table_length]; // 捕获异常表
u2 attributes_count; // 嵌套属性数量
attribute_info attributes[attributes_count]; // 嵌套属性
}

code 属性一般由两个常见的子属性, 分别是:

  • LineNumberTable
  • LocalVariableTable

4.3.2 LocalLineTable

通过该属性可以完成字节码与 Java 源码的行号映射
可以在 debug 的时候准确找到源码
并且抛出异常的时候堆栈信息可以找到对应行号
1
2
3
4
5
6
7
8
9
10
LineNumberTable_arrtibute {
u2 attribute_name_index; // 指向常量池, 必须是值为 "LineNumberTable" 的 Utf8 常量
u4 arrtibute_length; // 当前属性长度, 不包括开始的 6 个字节
u2 line_number_table_length; // line_number_table 数组元素个数

{
u2 start_pc; // start_pc 值必须是 code[] 数组的一个索引
u2 line_number; // 源文件的行号
} line_number_table[line_number_table_length];
}

4.3.3 LocalVariableTable

LocalVariableTable 属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。 

每个 LocalVariableTable 的 local_variable_table 部分可以看做是一个数组, 每个数组项是一个叫做local_variable_info的结构, 该结构描述了某个局部变量的变量名和描述符, 还有和源代码的对应关系。

下面讲解 local_variable_info 的各个部分:

  • start_pc 是当前 local_variable_info 所对应的局部变量的作用域的起始字节码偏移量;
  • length 是当前 local_variable_info 所对应的局部变量的作用域的大小。 也就是从字节码偏移量 start_pcstart_pc+length 就是当前局部变量的作用域范围;
  • name_index 指向常量池中的一个 CONSTANT_Utf8_info , 该 CONSTANT_Utf8_info 描述了当前局部变量的变量名;
  • descriptor_index 指向常量池中的一个 CONSTANT_Utf8_info , 该 CONSTANT_Utf8_info 描述了当前局部变量的描述符;
  • index 描述了在该方法被执行时,当前局部变量在栈中局部变量表中的位置。

由此可知, 方法中的每个局部变量都会对应一个local_variable_info 。

1
2
3
4
5
6
7
8
9
10
11
12
13
LocalVariableTable_attribute {
u2 attribute_name_index; // 指向常量池, 必须是值为 "LocalVariableTable_attribute" 的 Utf8 常量
u4 attribute_length; // 当前属性长度, 不包括开始的 6 个字节
u2 local_variable_table_length; // local_variable_table[] 的元素个数

{
u2 start_pc; // 局部变量的索引都在范围 [start_pc, start_pc + length)
u2 length;
u2 name_index; // 变量名索引, 在常量池中
u2 descriptor_index; // 变量描述索引(在常量池中)
u2 index; // 此局部变量在当前栈帧的局部变量表中的索引
} local_variable_table[local_variable_table_length]
}

解析以上字节码得到:

start pc length slot name descript
0 15 0 this Lorg/destiny/jvm/model/Employee
0 15 1 name Ljava/lang/String
0 15 2 age I

在解析 code 属性时需要注意的两点:

  • code 属性中包含了方法真正的字节码
  • code 属性中包含几个子属性, 包括 LineNumberTable, LocalVariableTable等, 也需要进行解析.

Field, Method, Attribute 三者中, 我们可以抽象出如下的关系:

4.3.4 Exceptions

如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来, 并且记录对应的catch{}块中的起始机器指令地址.
当运行时在try块中有异常抛出的话, JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转.
1
2
3
4
5
6
7
u2 exception_table_length;          // 捕获异常表的长度
{
u2 start_pc; // 捕获起始地址
u2 end_pc; // 捕获结束地址
u2 handler_pc; //
u2 catch_type; // 异常类型
} exception_table[exception_table_length]; // 捕获异常表

exception_table 记录了该 code 属性中所有显示抛出的异常信心, 包括异常的作用于及类型.

5. 字节码指令

5.1 main 方法字节码

Employee 的 main 方法:

1
2
3
4
public static final main(String[] args) {
Employee employee = new Employee("destiny", 24);
employee.sayHello();
}

经过编译后的字节码:

5.1.1 new

new indexbyte1 indexbyte2
  • 操作: 创建一个对象
  • (indexbyte1 << 8) | indexbyte2 得到一个指向常量池的索引
  • BB 00 01 对应 new #1, 对应的类就是 org/destiny/jvm/model/Employee

  1. 在堆中创建一个新对象
  2. 将该对象的引用压入栈中

5.1.2 dup

  • 操作: 复制操作数栈栈顶的值, 并压入栈中

5.1.3 ldc

ldc index
  • 操作: 从运行时常量池中提取数据压入栈中
  • ldc #43, 43 在常量池中的值为字符串 destiny

5.1.4 bipush

bipush byte
  • 将有符号 byte 扩展为一个 int 类型的值 value, 然后将 value 压入到操作数栈中.
  • byte 是一个立即数而非常量池引用

5.1.5 invokespecial indexbyte1 indexbyte2

  • 操作: 对一个对象进行初始化, 父类的初始化, 调用私有方法(因为没有多态性为)
  • (indexbyte1 << 8) | indexbyte2 得到一个指向常量池的索引
  • invokespecial #45
  • 常量池 #45 是一个 methodref: <init>:(Ljava/lang/String;I)V
  • 需要形成新的栈帧

5.1.6 astore_n

  • 操作: 将栈顶的 reference 类型数据保存到局部变量表中
  • astore_0
  • astore_1
  • astore_2
  • astore_3

5.1.7 aload_n

  • 操作: 从局部变量表中加载一个 reference 类型的值到操作数栈中
  • aload_0
  • aload_1
  • aload_2
  • aload_3

5.1.8 invokevirtual indexbyte1 indexbyte2

  • 操作: 调用实例方法, 依据实例的具体类型进行分派(多态)
  • (indexbyte1 << 8) | indexbyte2
  • invokevirtual #47 => sayHello: ()V
  • 也需要形成新的栈帧

5.1.9 return

  • 操作: 方法返回, 从当前函数栈帧退出, 无返回值.

5.2 方法指令

1
2
3
4
public Employee(String name, int age) {
this.name = name;
this.age = age;
}

5.2.1 aload_0

  • 操作: 从局部变量表中加载 index 为 0 的 reference 类型的值到操作数栈中

5.2.2 aload_1

5.2.3 putfield indexbyte1 indexbyte2

  • 操作: 给一个对象字段赋值
  • (indexbyte1 << 8) | indexbyte2
  • putfield #15 => putfield name:Ljava/lang/String

5.3.3 iload_2

  • 操作: 从局部变量中把 index 为 2 的 int 类型的值加载到操作数栈中
  • reference 类型使用 aload, int 类型使用 iload

5.4 字节码指令的设计实现

使用 命令模式 来抽象该场景, 即将所有字节码指令抽象为命令对象, 基类声明 command 方法, 再根据操作数的不同泛化出不同的抽象子类

6 JVM 执行引擎

6.1 Java 命令

1
java -cp path1;path2 org.destiny.jvm.Employee
  • cp: classpath(s), 默认是当前路径
  • class name: 系统需要找到这个类的 main 方法, 然后执行它的字节码

6.2 执行过程

  • 加载类
    • 工具: ClassFileLoader
    • 目的地: 方法区
  • 获取类的 public static void main(String[] args) 方法
    • 从方法区寻找
  • 执行 main 方法的字节码
    • 字节码指令
    • 栈帧(StackFrame)
    • 堆(Heap)

6.3 字节码指令的分类

类型 指令
依次执行 new
bipush
ldc
dup
暂停当前栈帧并创建新栈帧 invokespecial
invokevirtual
跳转到另一行去执行 if_icmp_ge
if_icmple
goto
退出当前栈帧 return

7. 垃圾回收机制

7.1 Java 对象的内存布局

  • MarkWord: 标注对象的元信息
    • GC 年龄
    • 锁的标志位
  • ClassPointer: 指向方法区的类信息的指针
  • InstanceData: 类实例对象的数据
    • 方法信息保存在方法区
  • padding: 填充

7.2 对象分配和垃圾回收

  • 对象优先分配在新生代
    • 如果 Eden 区没有足够的空间, 则触发一次 MinorGC
    • Java 对象大多具有生命周期短暂的特点, MinorGC 非常频繁, 速度也很快
  • 大对象直接进入老年代
    • 可以根据参数设置阈值
  • 长期存活对象进入老年代
    • 每个对象都有一个年龄(age), 在 MarkWord 中
    • 如果 age 超过阈值, 则晋升到老年代
  • 动态年龄判断
    • 如果在 Survivor 空间中相同年龄的所有对象大小的总数和大于 Survivor 空间的一半, 年龄大于或等于该年龄的对象可以直接进入老年代

MinorGC 时 新生代与老年代的关系

>