JVM(六)如何执行方法调用

重写和重载

重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同而且参数列表也相同的方法之间的关系 。

public class OneOverride {

    //=========================
    // 这两个方法构成重载

    public void show(){

    }

    public void show(String str) {

    }

    //===============================

}


/**
 * 重写父类方法
 */
public class OneOverriderChilden extends  OneOverride{
	
    public void show(String str) {

    }
}

java 虚拟机识别方法的关键在于类名,方法名以及方法描述符(method descriptor),方法描述符,它是由方法的参数类型以及返回类型所构成。

方法调用

Java中的方法调用分为两大类:

1、解析调用(Resolution): 在类加载的解析阶段,会把其中的一部分符号引用转化为直接引用。 前提是:方法在程序运行之前,就有一个可确定的调用版本,且该版本在运行期不可变。即“编译期可知,运行期不变”,符合这个要求的主要包括静态方法私有方法两大类,前者与类型直接关联,后者外部无法调用,因此无法通过继承重写。

2、分派调用(Dispatch):又分为 “静态分派” “动态分派” “多分派” “单分派”。在运行期间才能确定调用方法的版本。

解析调用

jvm 字节码调用指令

jvm 提供了5条调用方法的字节码指令,分别是 :

  • invokestatic: 调用静态方法
  • invokespecial: 调用实例构造器方法、私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时确定一个实现此接口的对象
  • invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后再执行

其中 invokestatic 和 invokespecial 在类加载阶段会把方法的符号引用解析成直接引用(内存地址入口),这类方法也称为非虚方法。

注意的是: final方法虽然是用invokevirtual来调用的,但是因为它无法被覆盖,是唯一的,不需动态解析的,所以它也是非虚方法。

来看个例子

public class StaticResolution {
    public static  void sayHello(){
        System.out.println("hello world");
    }
    public static void main(String[] args) {
        StaticResolution.sayHello();
    }
}

这里调用了静态方法,那么使用 javap -v XX应该会使用 invokestatic

[root@iZm5e7bivgszquxjh18i39Z jvm测试]# javap -v StaticResolution
Classfile /home/jvm测试/StaticResolution.class
  Last modified Mar 4, 2020; size 504 bytes
  MD5 checksum f2bbab54fb03714e2332b782be397bfb
  Compiled from "StaticResolution.java"
public class StaticResolution
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            // hello world
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Methodref          #6.#23         // StaticResolution.sayHello:()V
   #6 = Class              #24            // StaticResolution
   #7 = Class              #25            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               sayHello
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               StaticResolution.java
  #17 = NameAndType        #8:#9          // "<init>":()V
  #18 = Class              #26            // java/lang/System
  #19 = NameAndType        #27:#28        // out:Ljava/io/PrintStream;
  #20 = Utf8               hello world
  #21 = Class              #29            // java/io/PrintStream
  #22 = NameAndType        #30:#31        // println:(Ljava/lang/String;)V
  #23 = NameAndType        #12:#9         // sayHello:()V
  #24 = Utf8               StaticResolution
  #25 = Utf8               java/lang/Object
  #26 = Utf8               java/lang/System
  #27 = Utf8               out
  #28 = Utf8               Ljava/io/PrintStream;
  #29 = Utf8               java/io/PrintStream
  #30 = Utf8               println
  #31 = Utf8               (Ljava/lang/String;)V
{
  public StaticResolution();
    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 1: 0

  public static void sayHello();
    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 hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 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 sayHello:()V
         3: return
      LineNumberTable:
        line 8: 0
        line 9: 3
}
SourceFile: "StaticResolution.java"

静态分派

在讲静态分派之前我们需要知道静态类型和动态类型,例如有以下程序 :

public class StaticDispatch {
    static abstract class Human{}
    static class Man extends  Human{}
    static class Woman extends  Human{}
    
    public void sayHello(Human guy){
        System.out.println("Hello human");
    }

    public void sayHello(Man guy){
        System.out.println("Hello man");
    }

    public void sayHello(Woman guy){
        System.out.println("Hello woman");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

输出 : 
Hello human
Hello human

上面的

Human man = new Man();

这里 “Human”是 man变量的 静态类型 (Static Type) 或者叫 外观类型(Apparent Type)而后面的 “Man” 则是 man 变量的 实际类型(Actual Type)。静态类型都实际类型在程序中都可以发生变化,** 区别在于静态类型的变化仅仅是在使用时发生,而其本身的静态类型并不发生改变。** 什么意思呢?就是 man 这个对象在被传作参数还是调用方法的时候,我们依然为会认为它是“Human”只有使用的时候它才是“Man”。

重载与静态分配

有三个关键点需要知道 :

  • 静态类型在编译期可知,而动态类型只有实际运行时能够获知。
  • 虚拟机是通过参数静态类型作为重载的判定依据
  • 静态分派发生在编译阶段 但是重载有时候也会选择困难--我应该选择哪个重载方法,例如 :
public class Overload {
    public static void sayHello(Object obj){
        System.out.println("Hello object");
    }
    public static void sayHello(int arg){
        System.out.println("Hello int");
    }
    public static void sayHello(long arg){
        System.out.println("Hello long");
    }
    public static void sayHello(Character arg){
        System.out.println("Hello character");
    }

    public static void sayHello(char ...arg){
        System.out.println("Hello char ...");
    }

    public static void sayHello(Serializable arg){
        System.out.println("Hello Serializable ");
    }

    public static void main(String[] args) {
        sayHello('a');
    }
}

输出 : 
Hello int

重载的规则:

  1. 自身类型匹配
  2. 是否是基本类型,是,考虑自动装拆箱
  3. 形参的继承关系与重载方法是否匹配
  4. 变长参数匹配

另外以下也是静态分配 :

public class ResolutionAndDispatch{
    static void sayHello(int arg){
        System.out.println("Hello int");
    }
    static void sayHello(char arg){
        System.out.println("Hello char");
    }
    public static void main(String[] args){
        ResolutionAndDispatch.sayHello('a’);
    }
}

分派调用

分派调用揭示了OOP多态性的一些最基本的体现。“重载”和“重写”,就是其中之一。 如下例子 :

public class DynamicDispatch {
    static abstract class Human{
        protected abstract void sayHello();
    }
    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 woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

可以看到子类重写了父类的方法。

[root@iZm5e7bivgszquxjh18i39Z jvm测试]# javap -v  DynamicDispatch
Classfile /home/jvm测试/DynamicDispatch.class
  Last modified Mar 4, 2020; size 514 bytes
  MD5 checksum 7c19cd382f0b914eac869cb42608314f
  Compiled from "DynamicDispatch.java"
public class DynamicDispatch
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // DynamicDispatch$Man
   #3 = Methodref          #2.#22         // DynamicDispatch$Man."<init>":()V
   #4 = Class              #24            // DynamicDispatch$Woman
   #5 = Methodref          #4.#22         // DynamicDispatch$Woman."<init>":()V
   #6 = Methodref          #12.#25        // DynamicDispatch$Human.sayHello:()V
   #7 = Class              #26            // DynamicDispatch
   #8 = Class              #27            // java/lang/Object
   #9 = Utf8               Woman
  #10 = Utf8               InnerClasses
  #11 = Utf8               Man
  #12 = Class              #28            // DynamicDispatch$Human
  #13 = Utf8               Human
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               DynamicDispatch.java
  #22 = NameAndType        #14:#15        // "<init>":()V
  #23 = Utf8               DynamicDispatch$Man
  #24 = Utf8               DynamicDispatch$Woman
  #25 = NameAndType        #29:#15        // sayHello:()V
  #26 = Utf8               DynamicDispatch
  #27 = Utf8               java/lang/Object
  #28 = Utf8               DynamicDispatch$Human
  #29 = Utf8               sayHello
{
  public DynamicDispatch();
    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 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
        36: return
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 16
        line 23: 20
        line 24: 24
        line 25: 32
        line 26: 36
}
SourceFile: "DynamicDispatch.java"
InnerClasses:
     static #9= #4 of #7; //Woman=class DynamicDispatch$Woman of class DynamicDispatch
     static #11= #2 of #7; //Man=class DynamicDispatch$Man of class DynamicDispatch
     static abstract #13= #12 of #7; //Human=class DynamicDispatch$Human of class DynamicDispatch

0~15 在做准备动作 我们看到调用了两次 invokespecial 是调用了实例构造器 构造了man 和woman两个实例,并且把他们的引用放在1、2个局部变量表Slot中接下来的16~21,16和20两句aload_1和aload_2 把创建的对象的引用压到栈顶,这两个对象是将要执行的方法sayHello()的执行者,称作接受者(Receiver) 17和21两句的方法调用指令 和参数 都是一样的,但是最终执行的目标方法不同,原因就是invokevirtual指令的多态查找

虚方法调用

java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用都会被编译成 invokeinterface 指令,这均属于java虚拟机中的需方法调用。 **java 虚拟机采取了一种空间换时间的策略来实现动态绑定。**它为每个类生成一个方法表,用以快速定位目标方法。

方法表

方法表满足两个特性 :

  • 子类表中包含父类表中的所有方法
  • 子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同

在执行过程中,java虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。 思考一下假如我们如果不使用方法表,我们就需要先去收集然后再查找目标方法了,但是即使使用了方法表还有没优化的空间呢?即时编译(JIT)还拥有另外两种性能更好的优化手段 : 内联缓存方法内联

内联缓存

它能够缓存虚方法中调用者的动态类型,以及该类型对应的目标方法。 在之后的执行过程中,如果碰到已缓存的类型,直接在缓存中找到对应的目标方法,没有找到,那么就会去方法表中寻找。

方法内联

后续讲解

补充

查看汇编后的java class

javap -v xxx

参考资料

  • https://tobiaslee.top/2017/02/14/Override-and-Overload/
  • 《深入JVM》课程
原文地址:https://www.cnblogs.com/Benjious/p/12416249.html