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

简介

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

字节码指令

基于堆栈

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

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

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

比如一条 a + b 指令:

  1. 基于寄存器:
add a, b
  1. 基于堆栈:
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

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

从以上可以总结字节码在解释运行的时候几个重要的数据结构

  • 局部变量
  • 操作数栈
  • 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 等操作局部变量的指令。

一个方法的局部变量数组的长度 = 方法参数长度 + 方法本地变量长度

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

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

  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
上一篇:Vue.js源码(2):初探List Rendering


下一篇:C# 实现gRPC通信