简介
miniJVM 作为一个 mini 的 Java VM,实现了 Switch 解释器,并不支持主流 JVM 的 JIT 或者更为复杂的 AOT。但这样对于我们了解字节码的执行已经足够了。
字节码指令
基于堆栈
字节码指令类似于汇编指令,但是不同的是:
- 一行汇编代码的格式一般都是 – opcode 操作数1 操作数2
- 然而字节码指令格式是 opcode + 栈
字节码的所有操作数都存在运行栈中,又叫操作数栈,所以可以看到字节码中存在大量的入栈出栈操作。这样做的好处在于更强的跨平台可能性,毕竟你不知道目标平台的寄存器状态或者数量。但是其缺点也是相当明显的:
比如一条 a + b 指令:
- 基于寄存器:
add a, b
- 基于堆栈:
load a load b ad
这样别人一条指令就能做完的操作,基于堆栈需要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个sol
从以上可以总结字节码在解释运行的时候几个重要的数据结构
- 局部变量
- 操作数栈
- 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,参数个数为
需要注意的时这里的 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
这里要注意的是,一个线程只需要一个操作数栈