简介
miniJVM 作为一个 mini 的 Java VM,实现了 Switch 解释器,并不支持主流 JVM 的 JIT 或者更为复杂的 AOT。但这样对于我们了解字节码的执行已经足够了。
字节码指令
基于堆栈
字节码指令类似于汇编指令,但是不同的是:
- 一行汇编代码的格式一般都是 - opcode 操作数1 操作数2
- 然而字节码指令格式是 opcode + 栈
字节码的所有操作数都存在运行栈中,又叫操作数栈,所以可以看到字节码中存在大量的入栈出栈操作。这样做的好处在于更强的跨平台可能性,毕竟你不知道目标平台的寄存器状态或者数量。但是其缺点也是相当明显的:
比如一条 a + b 指令:
- 基于寄存器:
add a, b
- 基于堆栈:
load a
load b
add
这样别人一条指令就能做完的操作,基于堆栈需要3条,前两条都是参数入栈操作
运行时的情况
由于此类知识网上已有很多,所以我图省事找了一个现成的例子:
- Java Code
4. public static int add(int a, int b) {
5. int c = 0;
6. c = a + b;
7. return c;
8. }
- 字节码
public static int add(int, int);
descriptor: (II)I //描述方法参数为两个int类型的变量和方法的返回类型是int的
flags: ACC_PUBLIC, ACC_STATIC //修饰方法public和static
Code:
stack=2, locals=3, args_size=2 //操作数栈深度为2,本地变量表容量为3,参数个数为2
0: iconst_0 //将int值0压栈
1: istore_2 //将int值0出栈,存储到第三个局部变量(slot)中
2: iload_0 //将局部变量表中第一个变量10压栈
3: iload_1 //将局部变量表中第一个变量20压栈
4: iadd //将操作数栈顶两个int数弹出,相加后再压入栈中
5: istore_2 //将栈顶的int数(30)弹出,存储到第三个局部变量(slot)中
6: iload_2 //将局部变量表中第三个变量压栈
7: ireturn //返回栈中数字30
LineNumberTable:
line 5: 0 //代码第5行对应字节码第0行
line 6: 2 //代码第6行对应字节码第2行
line 7: 6 //代码第7行对应字节码第6行
LocalVariableTable:
Start Length Slot Name Si
0 8 0 a I //a占用第1个solt
0 8 1 b I //b占用第2个solt
2 6 2 c I //c占用第3个solt
从以上可以总结字节码在解释运行的时候几个重要的数据结构
- 局部变量
- 操作数栈
- PC 指针
- 行号表
- 指令序列
- 常量池
数据结构
方法栈
方法栈是方法运行的最基本数据结构
- 在 native 代码其实是一个栈帧,用于保存所有本地变量,部分方法参数以及方法跳转时保存寄存器的值
- 但是在 java 世界中,方法栈虽然也会保存类似的数据,但远不止这些
miniJVM 中方法栈叫做 Runtime
struct _Runtime {
//方法结构体
MethodInfo *method;
//类结构体
JClass *clazz;
//pc 指针
u8 *pc;
//方法字节码
CodeAttribute *ca;//method bytecode
//当前线程信息
JavaThreadInfo *threadInfo;
//子方法 runtime,类似栈
Runtime *son;//sub method's runtime
//父方法 runtime
Runtime *parent;//father method's runtime
//JVM 运行栈,用于基于栈实现的解释器
RuntimeStack *stack;
//方法本地变量
LocalVarItem *localvar;
//Runtime 缓存
union {
Runtime *runtime_pool_header;// cache runtimes for performance
Runtime *next; //for runtime pools linklist
};
//JNI 结构体
JniEnv *jnienv;
s16 localvar_count;
s16 localvar_max;
u8 wideMode;
};
- Runtime 初始化
Runtime 在一个线程中是一个链表,每跳转到一个方法则往后连一个节点,线程的第一个 Runtime 额外持有当前运行线程的结构体和操作数栈。
/**
* runtime 的创建和销毁会极大影响性能,因此对其进行缓存
* @param parent runtime of parent
* @return runtime
*/
static inline Runtime *runtime_create_inl(Runtime *parent) {
Runtime *top_runtime = NULL;
Runtime *runtime = NULL;
if (parent) {
top_runtime = parent->threadInfo->top_runtime;
}
if (top_runtime) {
runtime = top_runtime->runtime_pool_header;
if (runtime) {
top_runtime->runtime_pool_header = runtime->next;
runtime->next = NULL;
}
}
if (runtime == NULL) {
runtime = jvm_calloc(sizeof(Runtime));
runtime->localvar = jvm_calloc(RUNTIME_LOCALVAR_SIZE * sizeof(LocalVarItem));
runtime->localvar_max = RUNTIME_LOCALVAR_SIZE;
runtime->jnienv = &jnienv;
if (parent) {
runtime->stack = parent->stack;
runtime->threadInfo = parent->threadInfo;
}
}
//如果是子方法
if (parent != NULL) {
runtime->parent = parent;
parent->son = runtime;
} else {
//如果是根方法,所谓根方法,就是线程的第一个方法
runtime->stack = stack_create(STACK_LENGHT);
runtime->threadInfo = threadinfo_create();
runtime->threadInfo->top_runtime = runtime;
}
return runtime;
}
局部变量
局部变量存储了方法运行时所有的局部变量,不仅服务于解释器;也是 GC 的重要依据,用于判断线程运行时持有了哪些引用。
这里要注意的是:局部变量的属性和 index 信息存储在局部变量表中,而运行时局部变量真正的值存储在一个局部变量数组结构中。两者不要搞混
局部变量表
局部变量表在类加载中加载 Code 属性的时候就已经被初始化
局部变量表长度 = 方法参数数量 + 本地变量数量
方法参数数量和本地变量数量记录在方法的 Code 属性中:
Code:
stack=2, locals=3, args_size=2 //操作数栈深度为2,本地变量表容量为3,参数个数为2
需要注意的时这里的 locals 已经等于参数 + 本地变量
回顾一下前面类加载的时候介绍的解析 Code 属性的一段:
//本地变量表,决定方法栈大小
typedef struct _LocalVarTable {
u16 start_pc;
u16 length;
u16 name_index;
u16 descriptor_index;
u16 index;
} LocalVarTable;
else if (utf8_equals_c(class_get_utf8_string(clazz, attribute_name_index), "LocalVariableTable")) {
s2c.c1 = attr->info[info_p++];
s2c.c0 = attr->info[info_p++];
ca->local_var_table_length = (u16) s2c.s;
ca->local_var_table = jvm_calloc(sizeof(LocalVarTable) * ca->local_var_table_length);
s32 j;
for (j = 0; j < ca->local_var_table_length; j++) {
s2c.c1 = attr->info[info_p++];
s2c.c0 = attr->info[info_p++];
ca->local_var_table[j].start_pc = s2c.s;
s2c.c1 = attr->info[info_p++];
s2c.c0 = attr->info[info_p++];
ca->local_var_table[j].length = s2c.s;
s2c.c1 = attr->info[info_p++];
s2c.c0 = attr->info[info_p++];
ca->local_var_table[j].name_index = s2c.s;
s2c.c1 = attr->info[info_p++];
s2c.c0 = attr->info[info_p++];
ca->local_var_table[j].descriptor_index = s2c.s;
s2c.c1 = attr->info[info_p++];
s2c.c0 = attr->info[info_p++];
ca->local_var_table[j].index = s2c.s;
}
}
运行时局部变量
运行时局部变量时存放指令操作数据的重要地点,相关的有 xload_n,x_store_n 等操作局部变量的指令。
一个方法的局部变量数组的长度 = 方法参数长度 + 方法本地变量长度
- 一个局部变量的数据结构
运行时局部变量存储了两个东西:
- 变量的类型
- 变量的值,值类型的真实值或者时实例的引用
typedef struct _StackEntry {
union {
s64 lvalue;
f64 dvalue;
f32 fvalue;
s32 ivalue;
__refer rvalue;
Instance *ins;
};
s32 type;
} StackEntry, LocalVarItem;
- 初始化:
static inline s32 localvar_init(Runtime *runtime, s32 count) {
if (count > runtime->localvar_max) {
jvm_free(runtime->localvar);
runtime->localvar = jvm_calloc(sizeof(LocalVarItem) * count);
runtime->localvar_max = count;
} else {
memset(runtime->localvar, 0, count * sizeof(LocalVarItem));
}
runtime->localvar_count = count;
return 0;
}
- 将参数值写入局部变量
在方法的第一行 Code 执行之前,解释器需要把传入的方法参数值写到局部变量中
也就是说方法执行初期,局部变量中只有方法参数的值,而且该值在数组的头部。
/**
* 把堆栈中的方法调用参数存入方法本地变量
* 调用方法前,父程序把函数参数推入堆栈,方法调用时,需要把堆栈中的参数存到本地变量
* @param method method
* @param father runtime of father
* @param son runtime of son
*/
static inline void _stack2localvar(MethodInfo *method, LocalVarItem *localvar, RuntimeStack *stack) {
s32 i_local = method->para_slots;
// memcpy(localvar, &stack->store[stack->size - i_local], i_local * sizeof(StackEntry));
StackEntry *store = stack->store;
s32 i;
for (i = 0; i < i_local; i++) {
localvar[i].lvalue = store[stack->size - (i_local - i)].lvalue;
localvar[i].type = store[stack->size - (i_local - i)].type;
}
stack->size -= i_local;
}
操作数栈
前面说过操作数栈是 JVM 用于代替寄存器的机制,里面存储了 JVM 指令的操作数,比如在执行 iadd (int 值二元加法)指令前,需要将两个待加 int 值先入操作数栈。
- 结构体
和上文本地变量一样
RuntimeStack
struct _StackFrame {
StackEntry *store;
s32 size;
s32 max_size;
};
typedef struct _StackEntry {
union {
s64 lvalue;
f64 dvalue;
f32 fvalue;
s32 ivalue;
__refer rvalue;
Instance *ins;
};
s32 type;
} StackEntry, LocalVarItem;
这里要注意的是,一个线程只需要一个操作数栈
//如果是根方法,所谓根方法,就是线程的第一个方法
runtime->stack = stack_create(STACK_LENGHT);
runtime->threadInfo = threadinfo_create();
runtime->threadInfo->top_runtime = runtime;
PC 指针
PC 指针指向当前方法中运行的 Code 行号
主要服务于一些非顺序跳转指令:
- 条件语句的分支跳转
- 循环语句的跳转
- 异常分支的跳转
- debug 行号控制
行号表
行号表记录了行号和代码 PC 指针的对应关系
主要服务于:
- 异常抛出代码的定位
- debug 单步调试的定位
//行号
typedef struct _line_number {
u16 start_pc;
u16 line_number;
} LineNumberTable;
指令序列
指令序列在一个方法中是一个顺序排列的指令集合
解释器从指令序列中取址执行。
方法执行流程
准备工作
//准备方法栈
Runtime *runtime = runtime_create_inl(pruntime);
runtime->method = method;
runtime->clazz = clazz;
while (clazz->status < CLASS_STATUS_CLINITING) {
class_clinit(clazz, runtime);
}
s32 method_sync = method->access_flags & ACC_SYNCHRONIZED;
// if (utf8_equals_c(method->name, "getMethod")) {
// s32 debug = 1;
// }
//操作数栈
RuntimeStack *stack = runtime->stack;
if (!(method->access_flags & ACC_NATIVE)) {
//拿出 Code
CodeAttribute *ca = method->converted_code;
if (ca) {
//初始化本地变量
localvar_init(runtime, ca->max_locals);
LocalVarItem *localvar = runtime->localvar;
//方法参数进入本地变量
_stack2localvar(method, localvar, stack);
s32 stackSize = stack->size;
//如果方法是同步的,加锁
if (method_sync)_synchronized_lock_method(method, runtime);
u8 *opCode = ca->code;
runtime->ca = ca;
JavaThreadInfo *threadInfo = runtime->threadInfo;
//调试相关
do {
runtime->pc = opCode;
u8 cur_inst = *opCode;
if (java_debug) {
//breakpoint
if (method->breakpoint) {
jdwp_check_breakpoint(runtime);
}
//debug step
if (threadInfo->jdwp_step.active) {//单步状态
threadInfo->jdwp_step.bytecode_count++;
jdwp_check_debug_step(runtime);
}
}
//process thread suspend
if (threadInfo->suspend_count) {
if (threadInfo->is_interrupt) {
ret = RUNTIME_STATUS_INTERRUPT;
break;
}
check_suspend_and_pause(runtime);
}
取指执行
这个 opCode 就是 pc 指针
这里用 Switch 分发,因为 Switch 直接使用 CPU 指令 跳转效率高,因此被称为 Switch 解释器。
/* ==================================opcode start =============================*/
#ifdef __JVM_DEBUG__
s64 inst_pc = runtime->pc - ca->code;
#endif
JUMP_TO_IP(cur_inst);
switch (cur_inst) {
label_nop:
case op_nop: {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("nop\n");
#endif
opCode += 1;
break;
}
label_aconst_null:
case op_aconst_null: {}
case op_xxxxx:{}
.........
Native 方法
如果待执行的是一个 native 方法
具体会在 JNI 篇详细描述
//本地方法
localvar_init(runtime, method->para_slots);//可能有非静态本地方法调用,因此+1
_stack2localvar(method, runtime->localvar, stack);
//缓存调用本地方法
if (!method->native_func) { //把本地方法找出来缓存
java_native_method *native = find_native_method(utf8_cstr(clazz->name), utf8_cstr(method->name),
utf8_cstr(method->descriptor));
if (!native) {
Instance *exception = exception_create_str(JVM_EXCEPTION_NOSUCHMETHOD, runtime,
utf8_cstr(method->name));
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
} else {
method->native_func = native->func_pointer;
}
}
if (method->native_func) {
if (method_sync)_synchronized_lock_method(method, runtime);
ret = method->native_func(runtime, clazz);
if (method_sync)_synchronized_unlock_method(method, runtime);
}
// if (utf8_equals_c(method->name, "nvgTextGlyphPositionsJni")) {
// int debug = 1;
// }
localvar_dispose(runtime);
JVM 指令
JVM 每一个指令基本都有几个类似的指令,比如像iconst、lconst、fconst、dconst 这些主要是针对不同的类型(int、long、float、double),将对应类型的值push到栈顶,其他指令类似。
JVM 指令大约可以分为 9 种:
- 本地变量操作指令
- 栈操作指令
- 常量操作指令
- 算术和逻辑操作指令
- 转换指令
- 对象,字段,方法操作指令
- 数组操作指令
- 跳转指令
- return
基本指令
x 有 i,l,f,d, a 代表(int、long、float、double、引用)
指令 | 描述 |
---|---|
xconst_n | x 型常量值n进栈 |
bipush | 将一个byte型常量值推送至栈顶 |
xload_n | 第n个x型局部变量进栈 |
xstore_n | 将栈顶x型数值存入第n个局部变量 |
xadd | 栈顶两x型数值相加,并且结果进栈 |
return | 当前方法返回void |
getstatic | 获取指定类的静态域,并将其值压入栈顶 |
putstatic | 为指定的类的静态域赋值 |
invokevirtual | 调用实例方法 |
invokespecial | 调用超类构造方法、实例初始化方法、私有方法 |
invokestatic | 调用静态方法 |
invokeinterface | 调用接口方法 |
new | 创建一个对象,并且其引用进栈 |
newarray | 创建一个基本类型数组,并且其引用进栈 |
本地变量操作指令
该指令负责操作数栈和本地变量表的数据交互工作,主要是
- 从本地变量表中取出某值压入操作数栈(只是复制,不会清空本地变量表中的值)
- 从操作数栈中弹出值到本地变量表中(会清空操作数栈中该值)
这里举个常见的例子:
依然是 c = a + b
- 首先 a 和 b 的值在本地变量表中
- 第一步用 load 指令将 a 和 b 从本地变量中压入操作数栈
- 执行 add 指令,add 指令将操作数栈的栈顶两个值相加并清空这两个操作数,产生的结果压入操作数栈顶
- 最后用 store 指令将运算结果存到本地变量表的 c 中
和上面一样,为了区分操作数类型,指令也根据不同类型开头
以 load 为例:
xload_n(n = 0~3)
x 有 i,l,f,d, a 代表(int、long、float、double、引用)
n 代表局部变量表中第 n 槽的值,这里取 0-3 ,这样就可以节省很多操作数所占用的字节码空间。
当 n 超过 3 时,则使用 xload n 这种指令 + 一元操作数的方式。
- VM 代码
static inline u8 *_op_ifload_n(u8 *opCode, RuntimeStack *stack, LocalVarItem *localvar, Runtime *runtime, s32 i) {
Int2Float i2f;
//从本地变量中 get 到
i2f.i = localvar_getInt(localvar, i);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("if_load_%d: push localvar(%d)= [%x]/%d/%f \n", i, i, i2f.i, i2f.i, i2f.f);
#endif
//push 到操作数栈
push_int(stack, i2f.i);
opCode += 1;
return opCode;
}
栈操作指令
该指令主要是对操作数栈内的一些操作
- 弹出某些值
- 复制栈中值到栈内
- 栈内某些值的交换
这里以复制指令 dup 为例,引用 new 对象的一个经典案例:
- Java Code
A a = new A();
- Byte Code
// operand stack:
// ...
new A // ..., ref
dup // ..., ref, ref
invokespecial A.<init>()V // ..., ref
astore_0
这里 dup 的必要性就体现出来了
当 new 完 A 后,new 指令将实例引用压入栈顶
紧接着就会调用 A 的无参构造函数,而 invokespecial 会清空栈顶的引用,这样的话接下来将 A 实例存到本地变量 a 的操作将无法完成,所以在调用 invokespecial 之前需要将实例引用复制一份。
- VM 代码
case op_dup: {
StackEntry entry;
//取得操作数栈栈顶的值
peek_entry(stack, &entry, stack->size - 1);
//将该值再压入操作数栈
push_entry(stack, &entry);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("dup\n");
#endif
opCode += 1;
break;
}
常量操作指令
该指令和简单,就是将我们程序中定义的各种常量入操作数栈已准备接下来的运算而已,和前面一样也需要区分常量的类型以及值的范围
以 int 为例:
当int取值-1~5采用 iconst 指令,取值-128~127采用bipush指令,取值-32768!32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令。
- VM 代码
case op_bipush: {
//此行 code 的第二个元素就是常量操作数
s32 value = (s8) opCode[1];
//常量入栈
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("bipush a byte %d onto the stack \n", value);
#endif
opCode += 2;
break;
}
算术和逻辑操作指令
该指令用于运算符运算和逻辑操作
- 加减乘除
- 与或操作
- 移位操作
- 大小相等比较等
与前面类似,不同数据类型也有不同的指令
以加法 IADD 为例:
弹出操作数栈顶两个操作数,相加后压入操作数栈顶
case op_iadd: {
s32 value1 = pop_int(stack);
s32 value2 = pop_int(stack);
s32 result = value1 + value2;
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("iadd: %d + %d = %d\n", value1, value2, result);
#endif
push_int(stack, result);
opCode += 1;
break;
}
lcmp 比较指令
弹出操作数比较
相等则结果为 0,大于则为 1,小于则为 - 1
case op_lcmp: {
s64 value1 = pop_long(stack);
s64 value2 = pop_long(stack);
s32 result = value2 == value1 ? 0 : (value2 > value1 ? 1 : -1);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("lcmp: %llx cmp %llx = %d\n", value2, value1, result);
#endif
push_int(stack, result);
opCode += 1;
break;
}
转换指令
各种类型强转的指令
比如
Int -> Float
Float -> Int
等等
case op_f2i: {
f32 value1 = pop_float(stack);
s32 result = (s32) value1;
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("f2i: %d <-- %f\n", result, value1);
#endif
push_int(stack, result);
opCode += 1;
break;
}
对象,字段,方法操作
这类指令基本是 Java 这类语言特有的
- Field 操作,Get/Set
- 方法调用,各种 Invoke
- InstanceOf 操作符
- New Instance
- 同步块的进和出
Field 操作
有 get/set field 和对应 static field 的 get/set static
Get Filed
- 从 opcode 中获取 Field 引用在类常量池中的 index
- 从操作数栈中弹出 Field 所在的对象实例,为空则抛出空指针
- 尝试直接从缓存中获取 Field
- 失败则先找到 Field 引用常量,再找到 Field
- 根据 Field 和实例加载值
- 如果是 Field 是原子则使用内存屏障
- 最后根据值的不同类型把值压入操作数栈
case op_getfield: {
//从 Code 中获取 Field 的 Index
Short2Char s2c;
s2c.c1 = opCode[1];
s2c.c0 = opCode[2];
//Field 所在的对象
Instance *ins = (Instance *) pop_ref(stack);
if (!ins) {
//如果对象为空,则抛出空指针异常
Instance *exception = exception_create(JVM_EXCEPTION_NULLPOINTER, runtime);
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
} else {
//先从前面加载的缓存中获取目标 Field 的信息
FieldInfo *fi = class_get_constant_fieldref(clazz, s2c.s)->fieldInfo;
if (!fi) {
//如果是空,那么该段应该没有加载过,先获取引用常量,然后通过引用常量找到真正的 Field
ConstantFieldRef *cfr = class_get_constant_fieldref(clazz, s2c.s);
fi = find_fieldInfo_by_fieldref(clazz, cfr->item.index, runtime);
cfr->fieldInfo = fi;
}
//从目标对象中获取 Field 值的指针
c8 *ptr = getInstanceFieldPtr(ins, fi);
//如果该 Field 是原子的
if (fi->isvolatile) {
//那么设置内存屏障,强制从内存中读取
barrier();
}
if (fi->isrefer) {
//如果是引用类型
push_ref(stack, getFieldRefer(ptr));
} else {
// check variable type to determine s64/s32/f64/f32
s32 data_bytes = fi->datatype_bytes;
//基本类型,只要关注大小
switch (data_bytes) {
case 4: {
push_int(stack, getFieldInt(ptr));
break;
}
case 1: {
push_int(stack, getFieldByte(ptr));
break;
}
case 8: {
push_long(stack, getFieldLong(ptr));
break;
}
case 2: {
if (fi->datatype_idx == DATATYPE_JCHAR)push_int(stack, getFieldChar(ptr));
else push_int(stack, getFieldShort(ptr));
break;
}
}
}
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
StackEntry entry;
peek_entry(stack, &entry, stack->size - 1);
s64 v = entry_2_long(&entry);
jvm_printf("%s: push %s.%s[%llx]\n", "getfield", utf8_cstr(clazz->name), utf8_cstr(fi->name), (s64)(intptr_t)ptr, v);
#endif
}
opCode += 3;
break;
}
Set Field
基本类似
if (fi->isrefer) {//垃圾回收标识
setFieldRefer(ptr, entry_2_refer(&entry));
} else {
s32 data_bytes = fi->datatype_bytes;
//非引用类型
switch (data_bytes) {
case 4: {
setFieldInt(ptr, entry_2_int(&entry));
break;
}
case 1: {
setFieldByte(ptr, entry_2_int(&entry));
break;
}
case 8: {
setFieldLong(ptr, entry_2_long(&entry));
break;
}
case 2: {
setFieldShort(ptr, entry_2_int(&entry));
break;
}
}
}
}
Get/Set Static
static field 则省略入栈 Instance 的过程
InstanceOf
只是遍历所有父类和接口比较
u8 instance_of(JClass *clazz, Instance *ins, Runtime *runtime) {
JClass *ins_of_class = ins->mb.clazz;
while (ins_of_class) {
if (ins_of_class == clazz || isSonOfInterface(clazz, ins_of_class->mb.clazz, runtime)) {
return 1;
}
ins_of_class = getSuperClass(ins_of_class);
}
return 0;
}
New Instance
case op_new: {
//Class 引用 index
Short2Char s2c;
s2c.c1 = opCode[1];
s2c.c0 = opCode[2];
u16 object_ref = s2c.s;
//Class 引用常量
ConstantClassRef *ccf = class_get_constant_classref(clazz, object_ref);
//获取类并加载初始化
if (!ccf->clazz) {
Utf8String *clsName = class_get_utf8_string(clazz, ccf->stringIndex);
ccf->clazz = classes_load_get(clsName, runtime);
}
JClass *other = ccf->clazz;
//创建实例
Instance *ins = NULL;
if (other) {
ins = instance_create(runtime, other);
}
push_ref(stack, (__refer) ins);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("new %s [%llx]\n", utf8_cstr(ccf->name), (s64)(intptr_t)ins);
#endif
opCode += 3;
break;
}
方法调用指令
JVM 中方法调用指令有:
STORE_ADDRESS(op_invokevirtual, label_invokevirtual);
STORE_ADDRESS(op_invokespecial, label_invokespecial);
STORE_ADDRESS(op_invokestatic, label_invokestatic);
STORE_ADDRESS(op_invokeinterface, label_invokeinterface);
STORE_ADDRESS(op_invokedynamic, label_invokedynamic);
- invokevirtual
调用虚方法,此调用需要动态匹配,目标是调用实例对象的顶层实现方法。
该指令重要的是需要根据目标对象实例找到合适的方法实现
- 依然是先搜索缓存
- 如果没有命中则需要遍历父类,一个个方法的比对方法名和方法描述,需要特别注意的是 JDK8 接口中可以有个 Default 实现方法,所以 JDK8 以后接口也要跟着一起搜索。
case op_invokevirtual: {
Short2Char s2c;
s2c.c1 = opCode[1];
s2c.c0 = opCode[2];
//此cmr所描述的方法,对于不同的实例,有不同的method
ConstantMethodRef *cmr = class_get_constant_method_ref(clazz, s2c.s);
//取得目标实例
Instance *ins = getInstanceInStack(clazz, cmr, stack);
if (ins == NULL) {
Instance *exception = exception_create(JVM_EXCEPTION_NULLPOINTER, runtime);
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
} else {
MethodInfo *m = NULL;
if (ins->mb.type & (MEM_TYPE_CLASS)) {
//如果实例是个类,那么就是调用类的静态方法
m = cmr->methodInfo;
} else {
//先从缓存中查找,key 为方法引用和目标实现类型
m = (MethodInfo *) pairlist_get(cmr->virtual_methods, ins->mb.clazz);
if (m == NULL) {
//无命中,则开始遍历父类搜索
m = find_instance_methodInfo_by_name(ins, cmr->name, cmr->descriptor, runtime);
pairlist_put(cmr->virtual_methods, ins->mb.clazz, m);//放入缓存,以便下次直接调用
}
}
#if _JVM_DEBUG_BYTECODE_DETAIL > 3
if (utf8_equals_c(cmr->clsName, "java/io/FileInputStream")
&& utf8_equals_c(cmr->name, "open")
// && utf8_equals_c(cmr->descriptor, "(Ljava/lang/String;)Ljava/lang/StringBuilder;")
) {
int debug = 1;
}
invoke_deepth(runtime);
jvm_printf("invokevirtual %s.%s%s {\n", utf8_cstr(m->_this_class->name), utf8_cstr(m->name), utf8_cstr(m->descriptor));
#endif
if (m) {
//执行匹配到的实现方法
ret = execute_method_impl(m, runtime, m->_this_class);
} else {
//没找到合适的方法,则抛出 NoSuchMethodException
Instance *exception = exception_create_str(JVM_EXCEPTION_NOSUCHMETHOD, runtime,
utf8_cstr(cmr->name));
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
}
#if _JVM_DEBUG_BYTECODE_DETAIL > 3
invoke_deepth(runtime);
jvm_printf("}\n");
#endif
}
opCode += 3;
break;
}
- invokespecial
调用实例初始化,父类初始化和私有方法
实现非常简单,直接根据方法引用找到目标方法调用
- invokestatic
调用类的静态方法
实现非常简单,直接根据方法引用找到目标方法调用
- invokeinterface
调用接口方法
流程几乎与上文 invokevirtual 相同
- invokedynamic
为了更好的支持动态类型语言,Java7 给 JVM 增加了一条新的字节码指令:invokedynamic。除此之外 invokedynamic 也被用到了 Java8 的 Lambda 表达式实现上。
这是 invoke 中最复杂的一个
invokedynamic 有 4 个操作数,暂时只有前两个有用,后两个暂时留做他用
opCode += 5;
加指令一共是 5 行 opcode
前两个操作数构成 index,指向类的常量池中的 ConstantInvokeDynamic 常量。
typedef struct _ConstantInvokeDynamic {
ConstantItem item;
u16 bootstrap_method_attr_index;
u16 nameAndTypeIndex;
} ConstantInvokeDynamic;
bootstrap_method_attr_index 又指向类属性中的
typedef struct BootstrapMethods_attribute {
u16 num_bootstrap_methods;
BootstrapMethod *bootstrap_methods;
} BootstrapMethodsAttr;
typedef struct _BootstrapMethod {
u16 bootstrap_method_ref;
u16 num_bootstrap_arguments;
u16 *bootstrap_arguments;
//cache
MethodInfo *make;
} BootstrapMethod;
每个类中都有一个 BootstrapMethodsAttr 集合,保存了所有的 BootstrapMethod
每一个 BootstrapMethod 都包含一个 bootstrap_method_ref 和n个 bootstrap_arguments。bootstrap_method_ref 是个常量池索引,指向一个 CONSTANT_MethodHandle_info。而每一个bootstrap_argument 也都是常量池索引
除此之外还有 MethodHandle 常量
//方法句柄常量
typedef struct _ConstantMethodHandle {
ConstantItem item;
u8 reference_kind;
u16 reference_index;
} ConstantMethodHandle;
reference_kind 是一个1到9之间的整数。reference_index是常量池索引,但具体索引的是什么类型的常量。
reference_kind:
constant_pool entry | reference_kind |
---|---|
CONSTANT_Fieldref_info | 1 (REF_getField), 2 (REF_getStatic), 3 (REF_putField), or 4 (REF_putStatic) |
CONSTANT_Methodref_info | 5 (REF_invokeVirtual), 6 (REF_invokeStatic), 7 (REF_invokeSpecial), or 8 (REF_newInvokeSpecial) |
CONSTANT_InterfaceMethodref_info | 9 (REF_invokeInterface) |
这里以 lambda 表达式举例
那么他的 reference_kind 应该是 REF_invokeStatic
reference_index 应该指向 java.lang.invoke.LambdaMetafactory.metafactory() 静态方法
那么调用 lambda 表达式的流程是:
- 由操作数合成的 index 在类常量池中找到 ConstantInvokeDynamic 常量
- 根据 ConstantInvokeDynamic.bootstrap_method_attr_index 在类属性中找到 BootstrapMethod
- 根据 BootstrapMethod.bootstrap_method_ref 在类常量池中得到 ConstantMethodHandle, lambda 表达式的 Handler 应该指向 java.lang.invoke.LambdaMetafactory.metafactory() 静态方法引用。
- 由 ConstantMethodHandle 找到所引用的静态方法 metafactory()
- 准备调用 metafactory() 方法的前3个参数,lookup,invokeName,invokeMethodType
- 根据 BootstrapMethod.num_bootstrap_arguments 遍历取出各个类型的参数
- 调用 MethodHandle 即 metafactory() 得到返回值 CallSite
- 调用虚拟机内部方法 org/mini/reflect/vm/LambdaUtil.getMethodInfoHandle(CallSite) 得到 finder 方法的地址
- 最后调用 finder 方法将 calsite.target(MethodHandle) 转换成 MethodInfo * pointer,这才是真正要调用方法的指针,并且存到 bootMethod->make 缓存中
- 最后的最后真正执行 lambda 表达式所指向的方法
case op_invokedynamic: {
//index
Short2Char s2c;
s2c.c1 = opCode[1];
s2c.c0 = opCode[2];
u16 id_index = s2c.s;
//get bootMethod struct
//根据 index 得到 ConstantInvokeDynamic 常量
ConstantInvokeDynamic *cid = class_get_invoke_dynamic(clazz, id_index);
//bootstrap_method_attr_index -> BootstrapMethod
BootstrapMethod *bootMethod = &clazz->bootstrapMethodAttr->bootstrap_methods[cid->bootstrap_method_attr_index];//Boot
if (bootMethod->make == NULL) {
/**
* run bootstrap method java.lang.invoke.LambdaMetafactory
*
* public static CallSite metafactory(MethodHandles.Lookup caller,
* String invokedName,
* MethodType invokedType,
* MethodType samMethodType,
* MethodHandle implMethod,
* MethodType instantiatedMethodType)
*
*
* to generate Lambda Class implementation specify interface
* and new a callsite
*/
//准备调用 metafactory() 方法的前3个参数,lookup,invokeName,invokeMethodType
//parper bootMethod parameter
Instance *lookup = method_handles_lookup_create(runtime, clazz);
push_ref(stack, lookup); //lookup
Utf8String *ustr_invokeName = class_get_constant_utf8(clazz, class_get_constant_name_and_type(clazz, cid->nameAndTypeIndex)->nameIndex)->utfstr;
Instance *jstr_invokeName = jstring_create(ustr_invokeName, runtime);
push_ref(stack, jstr_invokeName); //invokeName
Utf8String *ustr_invokeType = class_get_constant_utf8(clazz, class_get_constant_name_and_type(clazz, cid->nameAndTypeIndex)->typeIndex)->utfstr;
Instance *mt_invokeType = method_type_create(runtime, ustr_invokeType);
push_ref(stack, mt_invokeType); //invokeMethodType
//other bootMethod parameter
//根据 BootstrapMethod.num_bootstrap_arguments 遍历取出各个类型的参数
s32 i;
for (i = 0; i < bootMethod->num_bootstrap_arguments; i++) {
ConstantItem *item = class_get_constant_item(clazz, bootMethod->bootstrap_arguments[i]);
switch (item->tag) {
case CONSTANT_METHOD_TYPE: {
ConstantMethodType *cmt = (ConstantMethodType *) item;
Utf8String *arg = class_get_constant_utf8(clazz, cmt->descriptor_index)->utfstr;
Instance *mt = method_type_create(runtime, arg);
push_ref(stack, mt);
break;
}
case CONSTANT_STRING_REF: {
ConstantStringRef *csr = (ConstantStringRef *) item;
Utf8String *arg = class_get_constant_utf8(clazz, csr->stringIndex)->utfstr;
Instance *spec = jstring_create(arg, runtime);
push_ref(stack, spec);
break;
}
case CONSTANT_METHOD_HANDLE: {
ConstantMethodHandle *cmh = (ConstantMethodHandle *) item;
MethodInfo *mip = find_methodInfo_by_methodref(clazz, cmh->reference_index, runtime);
Instance *mh = method_handle_create(runtime, mip, cmh->reference_kind);
push_ref(stack, mh);
break;
}
default: {
jvm_printf("invokedynamic para parse error.");
}
}
}
//get bootmethod
//s32 reference_kind = class_get_method_handle(clazz, bootMethod->bootstrap_method_ref)->reference_kind;
//bootstrap_method_ref -> ConstantMethodHandle -> metafactory() 的 MethodInfo
MethodInfo *boot_m = find_methodInfo_by_methodref(clazz, class_get_method_handle(clazz, bootMethod->bootstrap_method_ref)->reference_index, runtime);
if (boot_m) {
//执行 metafactory() 得到 CallSite
ret = execute_method_impl(boot_m, runtime, boot_m->_this_class);
if (ret == RUNTIME_STATUS_NORMAL) {
//调用虚拟机内部方法 org/mini/reflect/vm/LambdaUtil.getMethodInfoHandle(CallSite) 得到 finder 方法的地址
MethodInfo *finder = find_methodInfo_by_name_c("org/mini/reflect/vm/LambdaUtil", "getMethodInfoHandle", "(Ljava/lang/invoke/CallSite;)J", runtime);
if (finder) {
//调用 finder 方法将 calsite.target(MethodHandle) 转换成 MethodInfo * pointer,这才是真正要调用方法的指针,并且存到 bootMethod->make 缓存中
//run finder to convert calsite.target(MethodHandle) to MethodInfo * pointer
ret = execute_method_impl(finder, runtime, finder->_this_class);
if (ret == RUNTIME_STATUS_NORMAL) {
MethodInfo *make = (MethodInfo *) (intptr_t) pop_long(stack);
bootMethod->make = make;
}
} else {
Instance *exception = exception_create(JVM_EXCEPTION_NOSUCHMETHOD, runtime);
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
}
}
} else {
Instance *exception = exception_create(JVM_EXCEPTION_NOSUCHMETHOD, runtime);
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
}
}
MethodInfo *m = bootMethod->make;
#if _JVM_DEBUG_BYTECODE_DETAIL > 3
invoke_deepth(runtime);
jvm_printf("invokedynamic | %s.%s%s {\n", utf8_cstr(m->_this_class->name),
utf8_cstr(m->name), utf8_cstr(m->descriptor));
#endif
if (ret == RUNTIME_STATUS_NORMAL) {
if (m) {
// run make to generate instance of Lambda Class
//真正执行 lambda 表达式所指向的方法
ret = execute_method_impl(m, runtime, m->_this_class);
} else {
Instance *exception = exception_create(JVM_EXCEPTION_NOSUCHMETHOD, runtime);
push_ref(stack, (__refer) exception);
ret = RUNTIME_STATUS_EXCEPTION;
}
}
#if _JVM_DEBUG_BYTECODE_DETAIL > 3
invoke_deepth(runtime);
jvm_printf("}\n");
#endif
opCode += 5;
break;
}
/**
* 返回c MethodInfo 地址
*
* @param callsite
* @return
*/
public static long getMethodInfoHandle(CallSite callsite) {
if (callsite != null && callsite.getTarget() != null) {
Method m = callsite.getTarget().getMethod();
return ReflectMethod.findMethod0(m.getDeclaringClass().getName(), m.getName(), m.getSignature());
} else {
return 0;
}
}
同步块进出指令
- monitorenter synchronized 代码块开始时调用
- monitorexit synchronized 代码块结束时调用
很简单,进入时对象加锁,退出时释放对象锁
case op_monitorenter: {
Instance *ins = (Instance *) pop_ref(stack);
jthread_lock(&ins->mb, runtime);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("monitorenter [%llx] %s \n", (s64)(intptr_t)ins, ins ? utf8_cstr(ins->mb.clazz->name) : "null");
#endif
opCode += 1;
break;
}
label_monitorexit:
case op_monitorexit: {
Instance *ins = (Instance *) pop_ref(stack);
jthread_unlock(&ins->mb, runtime);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("monitorexit [%llx] %s \n", (s64)(intptr_t)ins, ins ? utf8_cstr(ins->mb.clazz->name) : "null");
#endif
opCode += 1;
break;
}
跳转指令
以相等指令为例 IF_ACMPEQ:
跳转的偏移地址保存在前指令的两个操作数中
case op_if_acmpeq: {
__refer v2 = pop_ref(stack);
__refer v1 = pop_ref(stack);
if (v1 == v2) {
//如果相等,从操作数中取出要跳转的地址
Short2Char s2c;
s2c.c1 = opCode[1];
s2c.c0 = opCode[2];
//opCode + 偏移跳转
opCode += s2c.s;
} else {
//否则往下执行
opCode += 3;
}
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("op_if_acmpeq: %lld == %lld \n", (s64)(intptr_t)v1, (s64)(intptr_t)v2);
#endif
break;
}
这里的实现解释了 Java 初学时的经典问题,两个对象比较相等,其实就是比较他们两个的地址
Return 指令
很简单,给返回值 ret 复值,则循环取指将被打断返回
case op_ireturn:
case op_lreturn:
case op_freturn:
case op_dreturn:
case op_areturn: {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
StackEntry entry;
peek_entry(stack, &entry, stack->size - 1);
invoke_deepth(runtime);
jvm_printf("ilfda_return=[%x]/%d/[%llx]\n", entry_2_int(&entry), entry_2_int(&entry), entry_2_long(&entry));
#endif
opCode += 1;
ret = RUNTIME_STATUS_RETURN;
break;
}
label_return:
case op_return: {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
invoke_deepth(runtime);
jvm_printf("return: \n");
#endif
opCode += 1;
ret = RUNTIME_STATUS_RETURN;
break;
}