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

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

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型常量值推送至栈顶
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_

这里 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;
                                    }
                                }
                            }
目录
相关文章
|
存储 Java 开发者
浅析JVM方法解析、创建和链接
上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。
585 132
|
监控 Java Spring
JVM如何监控某个方法的入参和相应结果?
JVM如何监控某个方法的入参和相应结果?
240 0
|
缓存 监控 算法
吃透 JVM 诊断方法与工具使用
【8月更文挑战第4天】深入了解并掌握JVM诊断需把握几大要点:1) 熟悉JVM内存模型,如堆、栈及方法区;2) 掌握垃圾回收机制与算法;3) 运用工具如`jps`(查看Java进程)、`jstat`(监控运行状态)、`jmap`(生成堆快照)、`jhat`(分析堆快照)、`jstack`(检查线程栈); 4) 利用专业工具如Eclipse Memory Analyzer分析堆转储文件查找内存泄漏; 5) 动态监控与调整JVM参数; 6) 结合日志分析性能瓶颈。通过实战案例加深理解,有效应对JVM性能问题。
206 5
|
监控 安全 Java
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
817 2
|
缓存 Prometheus 监控
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
376 3
|
缓存 监控 算法
Java面试题:讨论JVM性能调优的常见方法和技巧。
Java面试题:讨论JVM性能调优的常见方法和技巧。
413 1
|
Java 编译器
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
290 1
|
监控 Java Android开发
吃透 JVM 诊断方法与工具使用
【8月更文挑战第3天】要精通JVM诊断,需掌握关键监控指标如内存(堆/非堆)、CPU使用及线程状态;熟悉工具如`jstat`(监控状态)、`jmap`(堆转储)、`jstack`(线程堆栈);并能利用Eclipse Memory Analyzer (MAT)分析堆转储找内存泄漏;同时理解GC日志以优化垃圾回收行为;通过实践案例加深理解。
181 0
|
JavaScript Java API
JAVA程序运行问题之JVM找到并开始执行main方法如何解决
JAVA程序运行问题之JVM找到并开始执行main方法如何解决
191 0
|
存储 设计模式 监控
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
218 0