Skip to content

JVM Lab

Posted on:2022.06.01

TOC

Open TOC

笔记

这里是软工一的笔记

https://docs.oracle.com/javase/specs/index.html

https://blog.csdn.net/luanlouis/category_9263262.html

https://doocs.github.io/jvm/

JVM 与字节码

Java 代码编译成字节码

字节码在虚拟机中执行

Java 虚拟机提供的语言无关性

JVM Run-Time Data Areas

https://blog.jamesdbloom.com/JVMInternals.html

https://www.cnblogs.com/czwbig/p/11127124.html

虚拟机对象探秘

对象的内存布局

https://www.cnblogs.com/zhengbin/p/6490953.html

对象的访问定位

访问数据

实例数据值(对象中各个实例字段的数据)

对象类型数据(对象类型、父类、实现的接口、方法等)

访问方式

句柄访问方式

直接指针访问方式

垃圾回收

https://www.cnblogs.com/czwbig/p/11127159.html

当没有引用类型变量指向某个对象在堆中所占用的内存空间时,该对象将无法再被访问,Java 中将其视为垃圾

垃圾回收申请

System.gc();

垃圾回收机制

垃圾回收器要负责完成 3 件任务

一般情况下,当垃圾回收器在进行回收操作的时候,整个应用的执行是被暂时中止 (stop-the-world) 的

垃圾回收机制最基本的做法是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成 3 个世代

对于不同的世代可以使用不同的垃圾回收算法

相关话题

https://www.jianshu.com/p/6060cc53aca7

类文件结构

https://louluan.blog.csdn.net/article/details/39892027

字节码指令集

https://en.wikipedia.org/wiki/Java_bytecode

虚拟机类加载

https://louluan.blog.csdn.net/article/details/50529868

https://www.cnblogs.com/czwbig/p/11127222.html

虚拟机字节码执行

栈帧结构

Java 指令与字节码

https://blog.jamesdbloom.com/JavaCodeToByteCode_PartOne.html

示例

基础

Demo.java
public class Demo
{
public static void main(String[] args)
{
int a = 1;
int b = 2;
int c = (a + b) * 5;
}
}
> javac Demo.java
// Demo.class
public class Demo {
public Demo() {
}
public static void main(String[] var0) {
byte var1 = 1;
byte var2 = 2;
int var3 = (var1 + var2) * 5;
}
}
> javap -v Demo.class
Classfile /C:/Users/VGalaxy/IdeaProjects/untitled/src/Demo.class
Last modified 2021425; size 275 bytes
MD5 checksum f494636c6ab184a19ac25dfaf7b6dd4c
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // Demo
super_class: #3 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // Demo
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 Demo.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 Demo
#14 = Utf8 java/lang/Object
{
public Demo();
descriptor: ()V
flags: (0x0001) 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 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: iconst_5
8: imul
9: istore_3
10: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 4
line 8: 10
}
SourceFile: "Demo.java"

方法调用

Demo.java
public class Demo {
public static int minus(int x) {
return -x;
}
public static void main(String[] args) {
int x = 5;
int y = minus(x);
}
}
> javap -v Demo.class
Classfile /C:/Users/VGalaxy/IdeaProjects/untitled/src/Demo.class
Last modified 2021629; size 334 bytes
MD5 checksum b597ee62b7f04bfd014e8fb33a3a94f3
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // Demo
super_class: #4 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Methodref #3.#16 // Demo.minus:(I)I
#3 = Class #17 // Demo
#4 = Class #18 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 minus
#10 = Utf8 (I)I
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Demo.java
#15 = NameAndType #5:#6 // "<init>":()V
#16 = NameAndType #9:#10 // minus:(I)I
#17 = Utf8 Demo
#18 = Utf8 java/lang/Object
{
public Demo();
descriptor: ()V
flags: (0x0001) 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 1: 0
public static int minus(int);
descriptor: (I)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ineg
2: ireturn
LineNumberTable:
line 4: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_5
1: istore_1
2: iload_1
3: invokestatic #2 // Method minus:(I)I
6: istore_2
7: return
LineNumberTable:
line 8: 0
line 9: 2
line 11: 7
}
SourceFile: "Demo.java"

实验攻略 旧

相关资源

https://minguw.gitbook.io/jvm/

https://github.com/wym0120/JVVM

Lab 4

ClassFileReader::readClassFile 顺序

Lab 5

DCMPG 和 DCMPL 中先比较是否相等(浮点数)

long → int

value : long
int low = (int) value;
int high = (int) (value >> 32);

写成如下形式则报错

value : long
int low = (int) (value & 0xffff);
int high = (int) ((value >> 32) & 0xffff);

另外不要混用 >>>>>

https://stackoverflow.com/questions/16763917/what-is-the-purpose-of-the-unsigned-right-shift-operator-in-java

The >>> operator lets you treat int and long as 32- and 64-bit unsigned integral types, which are missing from the Java language. This is useful when you shift something that does not represent a numeric value. For example, you could represent a black and white bit map image using 32-bit int s, where each int encodes 32 pixels on the screen.

int → long

勿忘显式类型转换

long value = (long) high << 32 | low;

Lab 6

TODO: doc

实验攻略 新

相关资源

https://amnore.github.io/VJVM/

Lab 1

overview

一堆花里胡哨的注解……

主要关注 Main.java 中的 Dump

class descriptor

考虑 loadClass 方法的参数 descriptor

形式为 Llab2/HelloWorld;

而对应需要寻找的类文件名称为 lab2/HelloWorld.class

所以实际上有关系

name = descriptor.substring(1, descriptor.length() - 1) + ".class";

searchPaths

关注 bootstrapLoadersearchPaths

/usr/lib/jvm/java-8-openjdk/jre/lib/resources.jar
/usr/lib/jvm/java-8-openjdk/jre/lib/rt.jar
/usr/lib/jvm/java-8-openjdk/jre/lib/sunrsasign.jar
/usr/lib/jvm/java-8-openjdk/jre/lib/jsse.jar
/usr/lib/jvm/java-8-openjdk/jre/lib/jce.jar
/usr/lib/jvm/java-8-openjdk/jre/lib/charsets.jar
/usr/lib/jvm/java-8-openjdk/jre/lib/jfr.jar
/usr/lib/jvm/java-8-openjdk/jre/classes

需要判断是否存在

dump

查看 class 文件的内容

$ javap -verbose Foo.class
Classfile /home/vgalaxy/Desktop/jvm-2022/testdata/build/lab1/cases/Foo.class
Last modified May 3, 2022; size 516 bytes
MD5 checksum e5b9fd2858772da3474376a69bbcc350
Compiled from "Foo.java"
class lab1.cases.Foo implements java.io.Closeable
minor version: 0
major version: 52
flags: (0x0020) ACC_SUPER
this_class: #10 // lab1/cases/Foo
super_class: #11 // java/lang/Object
interfaces: 1, fields: 4, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #11.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #10.#31 // lab1/cases/Foo.a:I
#3 = String #32 // foo
#4 = Fieldref #10.#33 // lab1/cases/Foo.c:Ljava/lang/String;
#5 = Float 3.0f
#6 = Fieldref #10.#34 // lab1/cases/Foo.d:F
#7 = Long 2l
#9 = Fieldref #10.#35 // lab1/cases/Foo.b:J
#10 = Class #36 // lab1/cases/Foo
#11 = Class #37 // java/lang/Object
#12 = Class #38 // java/io/Closeable
#13 = Utf8 a
#14 = Utf8 I
#15 = Utf8 b
#16 = Utf8 J
#17 = Utf8 c
#18 = Utf8 Ljava/lang/String;
#19 = Utf8 d
#20 = Utf8 F
#21 = Utf8 ConstantValue
#22 = Utf8 <init>
#23 = Utf8 ()V
#24 = Utf8 Code
#25 = Utf8 LineNumberTable
#26 = Utf8 close
#27 = Utf8 <clinit>
#28 = Utf8 SourceFile
#29 = Utf8 Foo.java
#30 = NameAndType #22:#23 // "<init>":()V
#31 = NameAndType #13:#14 // a:I
#32 = Utf8 foo
#33 = NameAndType #17:#18 // c:Ljava/lang/String;
#34 = NameAndType #19:#20 // d:F
#35 = NameAndType #15:#16 // b:J
#36 = Utf8 lab1/cases/Foo
#37 = Utf8 java/lang/Object
#38 = Utf8 java/io/Closeable
{
int a;
descriptor: I
flags: (0x0000)
static long b;
descriptor: J
flags: (0x0008) ACC_STATIC
final float d;
descriptor: F
flags: (0x0010) ACC_FINAL
ConstantValue: float 3.0f
lab1.cases.Foo();
descriptor: ()V
flags: (0x0000)
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: aload_0
10: ldc #3 // String foo
12: putfield #4 // Field c:Ljava/lang/String;
15: aload_0
16: ldc #5 // float 3.0f
18: putfield #6 // Field d:F
21: return
LineNumberTable:
line 5: 0
line 6: 4
line 8: 9
line 9: 15
public void close();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc2_w #7 // long 2l
3: putstatic #9 // Field b:J
6: return
LineNumberTable:
line 7: 0
}
SourceFile: "Foo.java"

dump 实际上实现了 javap 命令的部分功能

Lab 2

overview

运行第一个 Java 程序来分析一下执行过程

传递参数

此处为

-cp /home/vgalaxy/Desktop/jvm-2022/testdata/build run lab2.HelloWorld
@Override
public Integer call() {
var ctx = new VMContext(parent.userClassPath);
if (debug) {
ctx.interpreter().step(0);
}
ctx.run(entryClass);
return 0;
}

构造 VMContext

此处 userClassPath 即为 /home/vgalaxy/Desktop/jvm-2022/testdata/build

void run(String entryClass) {
var initThread = new JThread(this);
threads.add(initThread);
var entry = userLoader.loadClass('L' + entryClass.replace('.', '/') + ';');
var mainMethod = entry.findMethod("main", "([Ljava/lang/String;)V");
assert mainMethod.jClass() == entry;
interpreter.invoke(mainMethod, initThread, new Slots(1));
}

构造线程 JThread,实际上就是一个 JFrame 的栈

加载 entryClass,此处即为 lab2.HelloWorld

找到对应的 main 方法

然后 interpreter 开始工作

public void invoke(MethodInfo method, JThread thread, Slots args) {
var frame = new JFrame(method, args);
thread.push(frame);
if (method.native_()) {
runNativeMethod(thread);
} else {
run(thread);
}
}

构造初始 JFrame

public JFrame(MethodInfo method, Slots args) {
jClass = method.jClass();
link = jClass.constantPool();
this.method = method;
if (method.native_()) {
stack = null;
pc = null;
vars = new Slots(args.size());
} else {
var code = method.code();
stack = new OperandStack(code.maxStack());
pc = new ProgramCounter(code.code());
vars = new Slots(code.maxLocals());
}
args.copyTo(0, args.size(), vars, 0);
}

在进行函数调用时,参数首先从操作数栈的栈顶弹出,然后被复制进被调用方法栈帧的局部变量表

此处局部变量表为 vars

然后线程开始运行

private void run(JThread thread) {
var frame = thread.top();
var monitor = thread.context().monitor();
while (thread.top() == frame) {
if (status == Status.STEP && steps == 0) {
monitor.enter(thread);
}
var op = Decoder.decode(thread.pc(), frame.method());
steps--;
op.run(thread);
// TODO(optional): handle breakpoints
}
}

弹出顶部的 JFrame

根据程序计数器和栈帧的方法构造对应的指令对象

public static Instruction decode(ProgramCounter pc, MethodInfo method) {
var opcode = Byte.toUnsignedInt(pc.byte_());
if (Decoder.decodeTable[opcode] == null) {
throw new UnimplementedInstructionError(opcode);
}
return Decoder.decodeTable[opcode].apply(pc, method);
}

然后调用每个指令对象的 run 方法即可

runtime env

https://doocs.github.io/jvm/01-jvm-memory-structure.html

terminal

在终端中运行

$ ./gradlew jar
$ java -jar build/libs/VJVM-0.0.1.jar -cp testdata/build run -d lab2.HelloWorld

记得切换为 jdk8

$ sudo archlinux-java set java-8-openjdk

slots

需要注意操作数栈对应 OperandStack 类,其中包含了 Slots

而局部变量表直接对应 Slots

使用如下方式进行模拟

private Object[] dataArray;
private SlotType[] typeArray;
private enum SlotType {
INT,
LONG,
FLOAT,
DOUBLE,
}

于是 LONGDOUBLE 会有一个 slot 是空的

另外不确定操作数栈在 popSlots 时是否需要逆向拷贝

因为 copyTo 只是正向拷贝

发现了 commit 记录

https://github.com/amnore/VJVM-public/commit/d78d33b2e54d720f18a8097cdf2bec5c2295fdf0

method descriptor

考虑 method 的 descriptor

先参数后返回值

例如 void main(String[] args) 的 descriptor 为

([Ljava/lang/String;)V

具体参考 MethodDescriptors 类和 Descriptors

IO

使用了 native 方法

debugger

加入 -d 选项来启动调试器

-cp /home/vgalaxy/Desktop/jvm-2022/testdata/build run -d lab2.HelloWorld

于是可以观察到对应的指令序列

> disas
0* bipush 72
2 invokestatic lab2/IOUtil:writeChar:(C)V
5 bipush 101
7 invokestatic lab2/IOUtil:writeChar:(C)V
10 bipush 108
12 invokestatic lab2/IOUtil:writeChar:(C)V
15 bipush 108
17 invokestatic lab2/IOUtil:writeChar:(C)V
20 bipush 111
22 invokestatic lab2/IOUtil:writeChar:(C)V
25 bipush 44
27 invokestatic lab2/IOUtil:writeChar:(C)V
30 bipush 32
32 invokestatic lab2/IOUtil:writeChar:(C)V
35 bipush 87
37 invokestatic lab2/IOUtil:writeChar:(C)V
40 bipush 111
42 invokestatic lab2/IOUtil:writeChar:(C)V
45 bipush 114
47 invokestatic lab2/IOUtil:writeChar:(C)V
50 iconst_1
51 invokestatic lab2/IOUtil:writeInt:(I)V
54 bipush 100
56 invokestatic lab2/IOUtil:writeChar:(C)V
59 return

简单实现断点功能后,可以这样打断点

> b lab2.HelloWorld main 50

源代码级调试器

指令与源代码行的映射

名称到局部变量表位置的映射

more opcode

https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-7.html

$ tree
.
├── comparisons
│ ├── IFCOND.java
│ ├── IF_XCMPCOND.java
│ ├── LCMP.java
│ └── XCMPCOND.java
├── constants
│ ├── LDCX.java
│ ├── NOP.java
│ ├── XCONST_Y.java
│ └── XPUSH.java
├── control
│ ├── GOTO.java
│ └── XRETURN.java
├── conversions
│ └── X2Y.java
├── loads
│ └── XLOAD.java
├── math
│ ├── IINC.java
│ ├── LIOPR.java
│ ├── XNEG.java
│ └── XOPR.java
├── references
│ ├── INVOKESTATIC.java
├── reserved
│ └── BREAKPOINT.java
├── stack
│ ├── DUPX.java
│ ├── DUPX_XY.java
│ ├── POPX.java
│ └── SWAP.java
└── stores
└── XSTORE.java

NOP.java

00 (0x00) nop

XCONST_Y.java

02 (0x02) iconst_m1
03 (0x03) iconst_0
04 (0x04) iconst_1
05 (0x05) iconst_2
06 (0x06) iconst_3
07 (0x07) iconst_4
08 (0x08) iconst_5
09 (0x09) lconst_0
10 (0x0a) lconst_1
11 (0x0b) fconst_0
12 (0x0c) fconst_1
13 (0x0d) fconst_2
14 (0x0e) dconst_0
15 (0x0f) dconst_1

XPUSH.java

16 (0x10) bipush
17 (0x11) sipush

LDCX.java

18 (0x12) ldc
19 (0x13) ldc_w
20 (0x14) ldc2_w

https://www.zhihu.com/question/296143618

生成新的 class 文件用于测试

$ javac -d ../../build Constant.java IOUtil.java
$ git restore ../../build/lab2/IOUtil.class

后来发现有测试用例 Conv.java

局部变量表 → 操作数栈

XLOAD.java

21 (0x15) iload
22 (0x16) lload
23 (0x17) fload
24 (0x18) dload
26 (0x1a) iload_0
27 (0x1b) iload_1
28 (0x1c) iload_2
29 (0x1d) iload_3
30 (0x1e) lload_0
31 (0x1f) lload_1
32 (0x20) lload_2
33 (0x21) lload_3
34 (0x22) fload_0
35 (0x23) fload_1
36 (0x24) fload_2
37 (0x25) fload_3
38 (0x26) dload_0
39 (0x27) dload_1
40 (0x28) dload_2
41 (0x29) dload_3

操作数栈 → 局部变量表

XSTORE.java

54 (0x36) istore
55 (0x37) lstore
56 (0x38) fstore
57 (0x39) dstore
59 (0x3b) istore_0
60 (0x3c) istore_1
61 (0x3d) istore_2
62 (0x3e) istore_3
63 (0x3f) lstore_0
64 (0x40) lstore_1
65 (0x41) lstore_2
66 (0x42) lstore_3
67 (0x43) fstore_0
68 (0x44) fstore_1
69 (0x45) fstore_2
70 (0x46) fstore_3
71 (0x47) dstore_0
72 (0x48) dstore_1
73 (0x49) dstore_2
74 (0x4a) dstore_3

生成新的 class 文件用于测试

$ javac -d ../../build LoadStore.java IOUtil.java
$ git restore ../../build/lab2/IOUtil.class

POPX.java

87 (0x57) pop
88 (0x58) pop2

DUPX.java

89 (0x59) dup
92 (0x5c) dup2

DUPX_XY.java

90 (0x5a) dup_x1
91 (0x5b) dup_x2
93 (0x5d) dup2_x1
94 (0x5e) dup2_x2

SWAP.java

95 (0x5f) swap

IINC.java

132 (0x84) iinc

LIOPR.java

120 (0x78) ishl
121 (0x79) lshl
122 (0x7a) ishr
123 (0x7b) lshr
124 (0x7c) iushr
125 (0x7d) lushr
126 (0x7e) iand
127 (0x7f) land
128 (0x80) ior
129 (0x81) lor
130 (0x82) ixor
131 (0x83) lxor

XNEG.java

116 (0x74) ineg
117 (0x75) lneg
118 (0x76) fneg
119 (0x77) dneg

XOPR.java

96 (0x60) iadd
97 (0x61) ladd
98 (0x62) fadd
99 (0x63) dadd
100 (0x64) isub
101 (0x65) lsub
102 (0x66) fsub
103 (0x67) dsub
104 (0x68) imul
105 (0x69) lmul
106 (0x6a) fmul
107 (0x6b) dmul
108 (0x6c) idiv
109 (0x6d) ldiv
110 (0x6e) fdiv
111 (0x6f) ddiv
112 (0x70) irem
113 (0x71) lrem
114 (0x72) frem
115 (0x73) drem

此时可以通过本地测试 Add.javaArith.java

X2Y.java

133 (0x85) i2l
134 (0x86) i2f
135 (0x87) i2d
136 (0x88) l2i
137 (0x89) l2f
138 (0x8a) l2d
139 (0x8b) f2i
140 (0x8c) f2l
141 (0x8d) f2d
142 (0x8e) d2i
143 (0x8f) d2l
144 (0x90) d2f
145 (0x91) i2b
146 (0x92) i2c
147 (0x93) i2s

IFCOND.java

153 (0x99) ifeq
154 (0x9a) ifne
155 (0x9b) iflt
156 (0x9c) ifge
157 (0x9d) ifgt
158 (0x9e) ifle

IF_XCMPCOND.java

159 (0x9f) if_icmpeq
160 (0xa0) if_icmpne
161 (0xa1) if_icmplt
162 (0xa2) if_icmpge
163 (0xa3) if_icmpgt
164 (0xa4) if_icmple

LCMP.java

148 (0x94) lcmp

XCMPCOND.java

149 (0x95) fcmpl
150 (0x96) fcmpg
151 (0x97) dcmpl
152 (0x98) dcmpg

GOTO.java

167 (0xa7) goto

XRETURN.java

172 (0xac) ireturn
173 (0xad) lreturn
174 (0xae) freturn
175 (0xaf) dreturn
177 (0xb1) return

此时可以通过全部本地测试

note

在实现指令的过程中有如下想法

否则 Slots 中对类型的检查可能会失败

opcode 占一个字节

offset 占两个字节

而 offset 是相对于 opcode 而言的

所以译码完成后,相对于跳转指令的 opcode 有 3 字节的偏移

显式分派

根据指令名称或者额外的枚举信息对同类指令进行不同的处理

主要体现在 OperandStack 的 push 和 pop 方法对不同类型的处理

典型的例子是 XCMPCONDX2Y

隐式分派

通过某种方式直接保存方法的类型信息

框架代码 XCONST_YXRETURN 是很好的范本

public class XRETURN<T> extends Instruction {
private final Function<OperandStack, T> popFunc;
private final Function<T, T> convertFunc;
private final BiConsumer<OperandStack, T> pushFunc;
private final String name;

这里的关键是,非静态方法的第一个参数实际上是 this 对象

所以这里函数式接口的描述符分别为

Function<OperandStack, T> // OperandStack -> T
BiConsumer<OperandStack, T> // (OperandStack, T) -> ()

XOPR 的实现便完全采用了这种技术

需要注意,这种技术并不是通用的

例如 X2Y 最初的实现采用了隐式分派,然而对于装箱后的基本类型,其强制类型转换是不允许的,遂仍采用显式分派

手册中描述了多种情形

其实只要全部视为一个 slot,保持相对顺序即可

Slots 类内部会对类型进行检查

对于手册中指令细节的描述,例如算术运算或类型转换的复杂性,其实根本不必细究

因为只要在代码中使用对应的运算符,编译器就会自动将其翻译为对应的指令,然后真实的 JVM 就会执行这些指令

VJVM 只是套了层皮……

todo