简介

miniJVM 作为一个 mini 的 Java VM,实现了 Switch 解释器,并不支持主流 JVM 的 JIT 或者更为复杂的 AOT。但这样对于我们了解字节码的执行已经足够了。

字节码指令

基于堆栈

字节码指令类似于汇编指令,但是不同的是:

  • 一行汇编代码的格式一般都是 – opcode 操作数1 操作数2
  • 然而字节码指令格式是 opcode + 栈

字节码的所有操作数都存在运行栈中,又叫操作数栈,所以可以看到字节码中存在大量的入栈出栈操作。这样做的好处在于更强的跨平台可能性,毕竟你不知道目标平台的寄存器状态或者数量。但是其缺点也是相当明显的:

比如一条 a + b 指令:

  1. 基于寄存器:
add a, b
  1. 基于堆栈:
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和staticCode:stack=2, locals=3, args_size=2     //操作数栈深度为2,本地变量表容量为3,参数个数为20: 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     //返回栈中数字30LineNumberTable:line 5: 0       //代码第5行对应字节码第0行line 6: 2       //代码第6行对应字节码第2行line 7: 6       //代码第7行对应字节码第6行LocalVariableTable:Start  Length  Slot  Name   Si0       8     0     a   I    //a占用第1个solt0       8     1     b   I    //b占用第2个solt2       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//父方法 runtimeRuntime *parent;//father method's runtime//JVM 运行栈,用于基于栈实现的解释器RuntimeStack *stack;//方法本地变量LocalVarItem *localvar;//Runtime 缓存union {Runtime *runtime_pool_header;// cache runtimes for performanceRuntime *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 等操作局部变量的指令。
一个方法的局部变量数组的长度 = 方法参数长度 + 方法本地变量长度

  • 一个局部变量的数据结构

运行时局部变量存储了两个东西:

  1. 变量的类型
  2. 变量的值,值类型的真实值或者时实例的引用
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)) {//拿出 CodeCodeAttribute *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) {//breakpointif (method->breakpoint) {jdwp_check_breakpoint(runtime);}//debug stepif (threadInfo->jdwp_step.active) {//单步状态threadInfo->jdwp_step.bytecode_count++;jdwp_check_debug_step(runtime);}}//process thread suspendif (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;
#endifJUMP_TO_IP(cur_inst);switch (cur_inst) {label_nop:case op_nop: {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5invoke_deepth(runtime);jvm_printf("nop\n");
#endifopCode += 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

  1. 首先 a 和 b 的值在本地变量表中
  2. 第一步用 load 指令将 a 和 b 从本地变量中压入操作数栈
  3. 执行 add 指令,add 指令将操作数栈的栈顶两个值相加并清空这两个操作数,产生的结果压入操作数栈顶
  4. 最后用 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 > 5invoke_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 > 5invoke_deepth(runtime);jvm_printf("dup\n");
#endifopCode += 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 > 5invoke_deepth(runtime);jvm_printf("bipush a byte %d onto the stack \n", value);
#endifopCode += 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 > 5invoke_deepth(runtime);jvm_printf("iadd: %d + %d = %d\n", value1, value2, result);
#endifpush_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 > 5invoke_deepth(runtime);jvm_printf("lcmp: %llx cmp %llx = %d\n", value2, value1, result);
#endifpush_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 > 5invoke_deepth(runtime);jvm_printf("f2i: %d <-- %f\n", result, value1);
#endifpush_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 的 IndexShort2Char 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) {//如果是空,那么该段应该没有加载过,先获取引用常量,然后通过引用常量找到真正的 FieldConstantFieldRef *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/f32s32 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 > 5invoke_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 引用 indexShort2Char 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 > 5invoke_deepth(runtime);jvm_printf("new %s [%llx]\n", utf8_cstr(ccf->name), (s64)(intptr_t)ins);
#endifopCode += 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所描述的方法,对于不同的实例,有不同的methodConstantMethodRef *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 > 3if (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));
#endifif (m) {//执行匹配到的实现方法ret = execute_method_impl(m, runtime, m->_this_class);} else {//没找到合适的方法,则抛出 NoSuchMethodExceptionInstance *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 > 3invoke_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;//cacheMethodInfo *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 表达式的流程是:

  1. 由操作数合成的 index 在类常量池中找到 ConstantInvokeDynamic 常量
  2. 根据 ConstantInvokeDynamic.bootstrap_method_attr_index 在类属性中找到 BootstrapMethod
  3. 根据 BootstrapMethod.bootstrap_method_ref 在类常量池中得到 ConstantMethodHandle, lambda 表达式的 Handler 应该指向 java.lang.invoke.LambdaMetafactory.metafactory() 静态方法引用。
  4. 由 ConstantMethodHandle 找到所引用的静态方法 metafactory()
  5. 准备调用 metafactory() 方法的前3个参数,lookup,invokeName,invokeMethodType
  6. 根据 BootstrapMethod.num_bootstrap_arguments 遍历取出各个类型的参数
  7. 调用 MethodHandle 即 metafactory() 得到返回值 CallSite
  8. 调用虚拟机内部方法 org/mini/reflect/vm/LambdaUtil.getMethodInfoHandle(CallSite) 得到 finder 方法的地址
  9. 最后调用 finder 方法将 calsite.target(MethodHandle) 转换成 MethodInfo * pointer,这才是真正要调用方法的指针,并且存到 bootMethod->make 缓存中
  10. 最后的最后真正执行 lambda 表达式所指向的方法
case op_invokedynamic: {//indexShort2Char 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 -> BootstrapMethodBootstrapMethod *bootMethod = &clazz->bootstrapMethodAttr->bootstrap_methods[cid->bootstrap_method_attr_index];//Bootif (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 parameterInstance *lookup = method_handles_lookup_create(runtime, clazz);push_ref(stack, lookup); //lookupUtf8String *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); //invokeNameUtf8String *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() 的 MethodInfoMethodInfo *boot_m = find_methodInfo_by_methodref(clazz, class_get_method_handle(clazz, bootMethod->bootstrap_method_ref)->reference_index, runtime);if (boot_m) {//执行 metafactory() 得到 CallSiteret = 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 * pointerret = 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 > 3invoke_deepth(runtime);jvm_printf("invokedynamic   | %s.%s%s {\n", utf8_cstr(m->_this_class->name),utf8_cstr(m->name), utf8_cstr(m->descriptor));
#endifif (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 > 3invoke_deepth(runtime);jvm_printf("}\n");
#endifopCode += 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 > 5invoke_deepth(runtime);jvm_printf("monitorenter  [%llx] %s  \n", (s64)(intptr_t)ins, ins ? utf8_cstr(ins->mb.clazz->name) : "null");
#endifopCode += 1;break;}label_monitorexit:case op_monitorexit: {Instance *ins = (Instance *) pop_ref(stack);jthread_unlock(&ins->mb, runtime);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5invoke_deepth(runtime);jvm_printf("monitorexit  [%llx] %s  \n", (s64)(intptr_t)ins, ins ? utf8_cstr(ins->mb.clazz->name) : "null");
#endifopCode += 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 > 5invoke_deepth(runtime);jvm_printf("op_if_acmpeq: %lld == %lld \n", (s64)(intptr_t)v1, (s64)(intptr_t)v2);
#endifbreak;}

这里的实现解释了 Java 初学时的经典问题,两个对象比较相等,其实就是比较他们两个的地址

Return 指令

很简单,给返回值 ret 复值,则循环取指将被打断返回

case op_ireturn:case op_lreturn:case op_freturn:case op_dreturn:case op_areturn: {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5StackEntry 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));
#endifopCode += 1;ret = RUNTIME_STATUS_RETURN;break;}label_return:case op_return: {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5invoke_deepth(runtime);jvm_printf("return: \n");
#endifopCode += 1;ret = RUNTIME_STATUS_RETURN;break;}

记一次 JVM 源码分析(4.解释器与方法执行)相关推荐

  1. java直接内存为什么快_直接内存与 JVM 源码分析

    直接内存(堆外内存) 直接内存有一种叫法,堆外内存. 直接内存(堆外内存)指的是 Java 应用程序通过直接方式从操作系统中申请的内存.这个差别与之前的堆.栈.方法区,那些内存都是经过了虚拟化.所以严 ...

  2. jvm源码分析之interrupt()

    概述 线程的thread.interrupt()方法是中断线程.中断一个线程意味着在线程完成它的任务之前,停止它当前正在执行的操作. 如果线程堵塞在object.wait.Thread.join和Th ...

  3. Spring AOP 源码分析 - 拦截器链的执行过程

    1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在我们的得 ...

  4. 【OkHttp】OkHttp 源码分析 ( 同步 / 异步 Request 请求执行原理分析 )

    OkHttp 系列文章目录 [OkHttp]OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 ) [OkHttp]Android 项目导入 OkHttp ( 配置依赖 | 配置 ...

  5. Java的wait()、notify()学习三部曲之一:JVM源码分析

    原文链接:https://blog.csdn.net/boling_cavalry/article/details/77793224 综述 Java的wait().notify()学习三部曲由三篇文章 ...

  6. 【Android 插件化】VirtualApp 源码分析 ( 启动应用源码分析 | HomePresenterImpl 启动应用方法 | VirtualCore 启动插件应用最终方法 )

    文章目录 一.启动应用源码分析 1.HomeActivity 启动应用点击方法 2.HomePresenterImpl 启动应用方法 3.VirtualCore 启动插件应用最终方法 一.启动应用源码 ...

  7. SaltStack源码分析之:master端执行salt模块大致流程

    2019独角兽企业重金招聘Python工程师标准>>> ##JOB执行流程 先看下官网对于master端的工作流程的介绍: The Salt master works by alwa ...

  8. asp.net mvc源码分析-Action篇 Action的执行

    接着上篇 asp.net mvc源码分析-Action篇 DefaultModelBinder 我们已经获取的了Action的参数,有前面的内容我们知道Action的调用时在ControllerAct ...

  9. Vue.js 源码分析(五) 基础篇 方法 methods属性详解

    methods中定义了Vue实例的方法,官网是这样介绍的: 例如:: <!DOCTYPE html> <html lang="en"> <head&g ...

最新文章

  1. 新计算机教师工作随笔反思,信息技术反思随笔 (2)
  2. ASP.NET Core WebAPI帮助页--Swagger简单使用1.0
  3. thinkpadt450s换键盘视频_ikbc c87 入门级有线机械键盘测评
  4. 【PL/SQL】学习笔记 (10)例外之 too_many_rows
  5. H5 活动利用Canvas把用户信息和二维码合并到图片内。
  6. iOS 手势解锁密码
  7. 与java的相识、相知、相惜、相恋、相守
  8. apache实验报告 linux_Apache服务器配置实验报告
  9. victoriaMetrics无法获取抓取target的问题
  10. 【华为OD统一考试B卷 | 100分】按身高和体重排队(C++ Java JavaScript Python)
  11. CSS3 SVG波浪线条动画js特效
  12. android10获取手机号码,【android,10】10.调用web服务获取手机号码归属地
  13. 计算方法(五)函数插值
  14. C语言-用栈实现表达式求值
  15. 软件测试用例设计方法(一)
  16. Case When和聚合函数count、sum的使用
  17. 月租最便宜的手机卡_有什么很划算的手机卡套餐?
  18. o.s.boot.SpringApplication : Application run failed错误
  19. u盘装puppy linux,将PuppyLinux安装到U盘
  20. python 动态壁纸

热门文章

  1. 洛谷P1465 [USACO2.2]序言页码 Preface Numbering
  2. iOS 10正式发布:十大新功能,更注重人性化
  3. WWDC22 - Apple 隐私技术探索
  4. win下使用fvm实现多个Flutter版本的切换
  5. nexus使用(建议收藏)
  6. 黑眼圈怎么才能快速遮瑕消除
  7. 抖音怎么直播?手机端和电脑端有什么不同
  8. iOS 网络资源汇总之UI
  9. Python文件重命名,中文名转英文名(含标点符号),改进版
  10. 【python数据挖掘课程】二十六.基于SnowNLP的豆瓣评论情感分析