欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

字节码执行引擎

程序员文章站 2022-05-12 14:22:11
...

一、函数解析

虚拟机方法字节码

在Java语言中符合“编译期可知,运行期不可变”。这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
与之相对应的是,在Java虚拟机里面提供了5条方法调用字节码指令,分别如下:

  • invokestatic 调用静态方法。
  • invokespecial 调用实例构造器<init>方法、 私有方法和父类方法。
  • invokevirtual 调用所有的虚方法。
  • invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条* 调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

静态类型和实际类型

如现在有Man类继承自父类Human,有代码如下:

Human man=new Man();

上面代码中的Human称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的Man则称为变量的实际类型(Actual Type)

虚方法

  • 非虚方法:只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类。
  • 虚方法:除去final方法和非虚方法,其他方法称为虚方法

虚方法表

虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表itable)使用虚方法表索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址。虚方法表一般是在类加载的阶段进行初始化。
字节码执行引擎

  • 如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口
  • 如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址

二、静态分派

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

public class StaticDispatch {
   static abstract class Human {
   }

   static class Man extends Human {
   }

   static class Woman extends Human {
   }

   // overload(多分派)
   public void sayHello(Human guy) {
      System.out.println("hello,guy!");
   }

   public void sayHello(Man guy) {
      System.out.println("hello,gentleman!");
   }

   public void sayHello(Woman guy) {
      System.out.println("hello,lady!");
   }


   public static void main(String[] args) {
      int i = 0;
      int m = 1;
      Human man = new Man();
      Human woman = new Woman();
      StaticDispatch sr = new StaticDispatch(); // new invokespecial
      // 下面两句函数调用是:运行时决定,还是编译时决定?
      // invokevirtual  静态分派 编译器决定
      sr.sayHello(man);     // invokevirtual
      sr.sayHello(woman);   // invokevirtual
   }
}

编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

三、动态分派

不是根据静态类型来确定,而是在运行时根据实际类型来决定函数的版本。

public class DynamicDispatch {
   static class Human {
      protected void sayHello(){
         System.out.println("Human");
      };
   }

   static class Man extends Human {
      @Override
      // 单分派
      protected void sayHello() {
         System.out.println("man say hello");
      }
   }

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

   public static void main(String[] args) {
      // 动态分派。不是根据静态类型来确定,而是在运行时根据实际类型来决定函数的版本。
      // 静态类型 Human man
      // 实际类型 new Man()
      Human man = new Man();       // invokespecial
      Human woman = new Woman();   // invokespecial
      man.sayHello();    // invokevirtual
      woman.sayHello();  // invokevirtual
      man = new Woman(); // new Woman
      man.sayHello();    // invokevirtual #n
   }
}

Java语言是一门静态多分派、 动态单分派的语言。

四、字节码

Java虚拟机的字节码指令由两部分组成

  • 操作码,也即Opcode。由一个字节长度的、 代表着某种特定操作含义的数字。由于操作码的长度只有一个字节,所以操作码总数不可能超过256条
  • 操作数,也即Operands。跟随在操作码之后的零至多个代表此操作所需参数。由于Java虚拟机采用面向操作数栈而不是寄存器架构,所以大多数的指令都不包含操作数,只有一个操作码。

字节码指令分类

  • 加法指令:iadd、 ladd、 fadd、 dadd
  • 减法指令:isub、 lsub、 fsub、 dsub
  • 乘法指令:imul、 lmul、 fmul、 dmul
  • 除法指令:idiv、 ldiv、 fdiv、 ddiv
  • 求余指令:irem、 lrem、 frem、 drem
  • 取反指令:ineg、 lneg、 fneg、 dneg
  • 位移指令:ishl、 ishr、 iushr、 lshl、 lshr、 lushr
  • 按位或指令:ior、 lor
  • 按位与指令:iand、 land
  • 按位异或指令:ixor、 lxor
  • 自增指令:iinc
  • 比较指令:dcmpg、 dcmpl、 fcmpg、 fcmpl、 lcmp
    字节码指令参考网址:https://segmentfault.com/a/1190000008722128
相关标签: 字节码执行引擎