记一次 JVM 源码分析(4.解释器与方法执行)_jvm 源码 loadlocalvariabletables-程序员宅基地

简介

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和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 等操作局部变量的指令。
一个方法的局部变量数组的长度 = 方法参数长度 + 方法本地变量长度

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

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

  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)) {
        //拿出 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

  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 > 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 表达式的流程是:

  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: {

                        //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;
                    }
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_33589510/article/details/105265210

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan