jvm 指令与指令执行

Geraltz'Rivia / 2023-08-21 / 原文

运行时内存结构

在了解jvm指令之前,需要先了解java虚拟机运行时内存的结构,根据jvm规范(java se8),运行时数据区给每个线程分配了不同的区域,比如PC寄存器、JVM栈,本地方法栈,线程共有的部分有堆与方法区。

栈上每个函数调用都有一个帧作为对应数据结构,每个帧都包括局部变量表与操作数栈,这部分在编译的时候就可以确定了,局部变量表用来保存方法中的局部变量,以及方法参数。当一个方法刚开始执行的时候,操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和取出数据,也就是入栈和出栈操作。

可以使用jclasslib来看到方法的局部变量表:

由于方法是static方法,所以局部变量表的第一个元素并不是this;

非static方法:

jvm指令执行

根据字节码解释器的工作流程:

do { 
  atomically calculate pc and fetch opcode at pc; 
  if (operands) 
    fetch operands; 
  	execute the action for the opcode; 
} while (there is more to do);

指令操作的对象有部分就是局部变量表上的数据,然后执行方法会在栈上进行计算,这种方式可以描述为基于栈的字节码解释执行。

比如一个简单的执行加法的函数:

    public int add(int i, int j) {
        return i+j;
    }

指令

0 iload_1

1 iload_2

2 iadd

3 ireturn

iload_1 iload_2 分别把两个局部变量表的内容放在栈上,然后执行iadd操作,取出栈顶两个元素,相加后放回栈顶,最后执行ireturn,返回栈顶的int

每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

 static abstract class Human {

        abstract void sayHello();
    }

    static class Man extends Human {

        @Override
        void sayHello() {
            System.out.println("Man say hello!");
        }
    }

    static class Woman extends Human {
        @Override
        void sayHello() {
            System.out.println("Woman say hello!");
        }
    }

    private static void testHumanSayHello() {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();

        man = new Woman();
        man.sayHello();
    }

编译出的字节码为

 0 new #3 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Man>
 3 dup
 4 invokespecial #4 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Man.<init> : ()V>
 7 astore_0
 8 new #5 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Woman>
11 dup
12 invokespecial #6 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Woman.<init> : ()V>
15 astore_1
16 aload_0
17 invokevirtual #7 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Human.sayHello : ()V>
20 aload_1
21 invokevirtual #7 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Human.sayHello : ()V>
24 new #5 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Woman>
27 dup
28 invokespecial #6 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Woman.<init> : ()V>
31 astore_0
32 aload_0
33 invokevirtual #7 <com/sankuai/example/rasp/pigeon/test/starter/util/ASMDemo$Human.sayHello : ()V>
36 return

可以看到在17与21行执行sayHello方法时,都是使用的invokevirtual,根据jvm虚拟机规范对指令的说明,执行invokevirtual,会把对象放在操作数栈的顶部,执行方法时先找到这个objectref对应的类C(从持有的字符串常量池的引用中查找),如果C中包含这个方法的重写方法,就调用,否则从下往上一次寻找类C的父类有无这个方法,如果有就调用。对于接口来说,需要找到一个方法实现并且方法描述里不包含abstract。

jvm的指令规范:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

参考

https://tobebetterjavaer.com/jvm/how-jvm-run-zijiema-zhiling.html

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5