工作生活

JVM_字节码:(栈帧与操作数栈)及(符号引用与直接引用)

2019-07-06  本文已影响0人  CalmHeart

局部变量表:

  1. 用于存储局部变量,都是用(slot)来描述局部变量的最小单位,32位int类型的数据,都会占用一个slot 对于long类型用2个连续的slot表示。
  2. slot是可以复用的,对于10个局部变量可能存在的slot<10 方法体可以存在更小的作用域,方法体内部的局部变量的作用域是不同的,在局部变量表不作区分,当b、c占据的slot结束了生命周期后,可能会被d、e占据,如:
public void test(){

         int a=3;

         if(a>4){
            int b=4;
            int c=5;
       }
      int d=7;
      int e=10;
}

动态链接是与C或者C++是不同的,对于C++来说Class之间的关系在编译期间就已经确定好了,包括地址的偏移量会提前设置好,所以存在动态链接库DLL Java是不同的,在编译期间Class之间方法的调用在加载或者在真正开始调用的时候才能确定,基于上述的方面存在符号和直接引用。

Animal a=new Cat();
a.sleep();//invokevirtual
a=new Dog();

Java方法调用的字节码指令:(5种)

1、invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定调用实现该接口的哪一个对象的特定方法。(需要定位实现类)
2、invokestatic: 调用静态方法。
3、invokespecial: 可以自己的私有方法(注意私有方法是不可以被重写),也可以是构造方法(<init>),也可以调用父类的方法(成员或者构造器)
4、invokevirtual: 调用虚方法(C++中是存在的),是和多态紧密相关的 也是运行期动态查找的过程 查找继承这个类或接口的方法。
5、invokedynamic: 动态调用方法。(1.7引入的)可以调用动态语言比如javascript。不是讨论的重点。


Java源文件: 用于观察static方法的调用

public class MyTest4 {
  public static void test(){
    System.out.println("test invoked");
  }
  public static void main(String[] args) {
    test();
  }
}

反编译结果:

C:\spring_lecture\target\classes\com\compass\spring_lecture\binarycode>javap -v MyTest4
警告: 二进制文件MyTest4包含com.compass.spring_lecture.binarycode.MyTest4
Classfile /C:/spring_lecture/target/classes/com/compass/spring_lecture/binarycode/MyTest4.class
  Last modified 2019-7-4; size 664 bytes
  MD5 checksum 123e058e19e1836c1250aa6b13b35182
  Compiled from "MyTest4.java"
public class com.compass.spring_lecture.binarycode.MyTest4
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #25            // test invoked
   #4 = Methodref          #26.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Methodref          #6.#28         // com/compass/spring_lecture/binarycode/MyTest4.test:()V
   #6 = Class              #29            // com/compass/spring_lecture/binarycode/MyTest4
   #7 = Class              #30            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/compass/spring_lecture/binarycode/MyTest4;
  #15 = Utf8               test
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               SourceFile
  #21 = Utf8               MyTest4.java
  #22 = NameAndType        #8:#9          // "<init>":()V
  #23 = Class              #31            // java/lang/System
  #24 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #25 = Utf8               test invoked
  #26 = Class              #34            // java/io/PrintStream
  #27 = NameAndType        #35:#36        // println:(Ljava/lang/String;)V
  #28 = NameAndType        #15:#9         // test:()V
  #29 = Utf8               com/compass/spring_lecture/binarycode/MyTest4
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (Ljava/lang/String;)V
{
  public com.compass.spring_lecture.binarycode.MyTest4();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/compass/spring_lecture/binarycode/MyTest4;

  public static void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String test invoked
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1

         0: invokestatic  #5                  // Method test:()V 通过使用invokestatic调用

         3: return
      LineNumberTable:
        line 15: 0
        line 17: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
}

方法调用的解析过程:invokestatic invokespecial是在解析的过程中就可以确定的,静态及自身父类的构造及成员方法。
静态解析的4种情形:

  1. 静态方法
  2. 父类方法
  3. 构造方法
  4. 私有方法(公有方法会被重写或者复写 因此存在多态)
    以上的4种方法就称为非虚方法,它们是在类加载阶段就可以将符号引用转换为直接引用。

方法重载(overload): 方法的参数类型或者参数的个数不同,签名的修饰符不算。

public class MyTest5 {

  public void test(Grandpa grandpa){
    System.out.println("grandpa");
  }

  public void test(Father father){
    System.out.println("father");
  }

  public void test(Son son){
    System.out.println("son");
  }

  public static void main(String[] args) {

    Grandpa p1=new Father();
    Grandpa p2=new Son();

    MyTest5 myTest5=new MyTest5();
    myTest5.test(p1);//grandpa
    myTest5.test(p2);//grandpa
  }
}
class Grandpa {
}

class Father extends Grandpa {
}

class Son extends Father {
}

上面的例子涉及到方法的静态分派:
g1声明的类型是Grandpa 是静态类型 g1的真正指向的类型是Father(实际类型)。
我们可以得到这样的一个结论:
变量的静态类型是不会发生改变的,而变量的实际类型是可以发生变化的,是多态的一种体现,实际类型是在运行期间才可以确定的。
方法的重载是一种纯粹静态的一种行为,对JVM来说,是根据声明的参数,而不是根据实际类型来决定的,是根据静态类型来进行匹配的。是在编译期间就可以完全确定的。
看一下的字节码:



重载和重写:重载和重写是不同的,重载是静态的,重写是动态的。体现在静态类型上面。


重写的代码示例:

public class MyTest6 {

//  apple
//  orange
//  orange
  public static void main(String[] args) {

    Furit apple = new Apple();
    Furit orange = new Orange();

    apple.test();
    orange.test();

    apple = new Orange();
    apple.test();

  }
}


class Furit {

  public void test() {

    System.out.println("fruit");

  }

}


class Apple extends Furit {

  @Override
  public void test() {

    System.out.println("apple");

  }
}

class Orange extends Furit {

  @Override
  public void test() {
    System.out.println("orange");

  }
}

编译生成的字节码:

public class com.compass.spring_lecture.binarycode.MyTest6 {
  public com.compass.spring_lecture.binarycode.MyTest6();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/compass/spring_lecture/binarycode/Apple
       3: dup
       4: invokespecial #3                  // Method com/compass/spring_lecture/binarycode/Apple."<init>":()V
       7: astore_1
       8: new           #4                  // class com/compass/spring_lecture/binarycode/Orange
      11: dup
      12: invokespecial #5                  // Method com/compass/spring_lecture/binarycode/Orange."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #6                  // Method com/compass/spring_lecture/binarycode/Furit.test:()V
      20: aload_2
      21: invokevirtual #6                  // Method com/compass/spring_lecture/binarycode/Furit.test:()V
      24: new           #4                  // class com/compass/spring_lecture/binarycode/Orange
      27: dup
      28: invokespecial #5                  // Method com/compass/spring_lecture/binarycode/Orange."<init>":()V
      31: astore_1
      32: aload_1
      33: invokevirtual #6                  // Method com/compass/spring_lecture/binarycode/Furit.test:()V
      36: return
}

方法的动态分派:
从上述的字节码看,方法的字节码是一致的,但是从实际的调用上看,它们是不同的。
方法的动态分派涉及到一个重要的概念:方法的接收者,也就是方法的调用者

涉及到invokevirtual字节码指令的多态查找流程:

  1. 到操作数栈顶上第一个元素并寻找栈顶元素所指向的实际类型,并不是静态类型。
  2. 如果获取到并且访问权限也是ok的,就直接调用并返回,如果获取不到按照继承体系进行查找,找到就调用。

比较方法重写和方法重载,我们得到这样一个结论,方法重载是静态的,是编译期行为,方法重写是动态的,是运行期行为。

动态分派的一个最直接的例子是重写。对于重写,我们已经很熟悉了,那么Java虚拟机是如何在程序运行期间确定方法的执行版本的呢?

解释这个现象,就不得不涉及Java虚拟机的invokevirtual指令了,这个指令的解析过程有助于我们更深刻理解重写的本质。该指令的具体解析过程如下:

  1. 找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为C

2 . 如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常

  1. 如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程

  2. 如果始终没有找到合适的方法,则抛出抽象方法错误的异常

从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。


虚方法表和动态分派机制:
针对于方法调用动态分派的过程:
虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table)简称vtable,
针对于invokeinterface指令来说,虚拟机会建立一个接口方法表的数据结构,(interface method table),简称itable。

上一篇下一篇

猜你喜欢

热点阅读