关于JVM中方法调用的相关指令,以及解析(Resolution)和分派(Dispatch)的解释——重载的实现原理与重写的实现原理

JVM中相关方法的调用的指令

invokestatic

调用静态方法。

invokespecial

用于调用构造器方法<init>、私有方法、父类方法。

invokevirtual

用于调用类的所有虚方法。

invokeinterface

用于调用接口方法。

解析(resolution)与分派(dispatch)

解析

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变

为可确定的直接引用,不会延迟到运行期再去完成。

下面我们看一段代码:

/**
 * 方法静态解析演示
 *
 * @author cxt
 */
public class StaticResolution {

	//静态方法
	public static void say1() {
	}

	//final方法
	public final void say2() {

	}

	//一般虚方法
	public void say3() {

	}
	//重载say3
	public void say3(int i) {

	}

	public static void main(String[] args) {
		StaticResolution.say1();
		StaticResolution sr = new StaticResolution();
		sr.say2();
		sr.say3();
		sr.say3(10);
	}

}

我们看一下字节码:

// class version 51.0 (51)
// access flags 0x21
public class StaticResolution {

  // compiled from: StaticResolution.java

  // access flags 0x1
  public <init>() : void
   L0
    LINENUMBER 8 L0
    ALOAD 0: this
    INVOKESPECIAL Object.<init> () : void
    RETURN
   L1
    LOCALVARIABLE this StaticResolution L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static say1() : void
   L0
    LINENUMBER 12 L0
    RETURN
    MAXSTACK = 0
    MAXLOCALS = 0

  // access flags 0x11
  public final say2() : void
   L0
    LINENUMBER 17 L0
    RETURN
   L1
    LOCALVARIABLE this StaticResolution L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1

  // access flags 0x1
  public say3() : void
   L0
    LINENUMBER 22 L0
    RETURN
   L1
    LOCALVARIABLE this StaticResolution L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1

  // access flags 0x1
  public say3(int) : void
   L0
    LINENUMBER 26 L0
    RETURN
   L1
    LOCALVARIABLE this StaticResolution L0 L1 0
    LOCALVARIABLE i int L0 L1 1
    MAXSTACK = 0
    MAXLOCALS = 2

  // access flags 0x9
  public static main(String[]) : void
  <strong><em style="text-decoration: underline;"> </em><span style="color:#ff6666;"><del>L0
    LINENUMBER 29 L0
    INVOKESTATIC StaticResolution.say1 () : void
   L1
    LINENUMBER 30 L1
    NEW StaticResolution
    DUP
    INVOKESPECIAL StaticResolution.<init> () : void
    ASTORE 1
   L2
    LINENUMBER 31 L2
    ALOAD 1: sr
    INVOKEVIRTUAL StaticResolution.say2 () : void
   L3
    LINENUMBER 32 L3
    ALOAD 1: sr
    INVOKEVIRTUAL StaticResolution.say3 () : void
   L4
    LINENUMBER 33 L4
    ALOAD 1: sr
    BIPUSH A    // '\n' (LF)
    INVOKEVIRTUAL StaticResolution.say3 (int) : void</del></span></strong>
   L5
    LINENUMBER 34 L5
    RETURN
   L6
    LOCALVARIABLE args String[] L0 L6 0
    LOCALVARIABLE sr StaticResolution L2 L6 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

我们看一下main方法的字节码,可知say1方法是static方法,所有它的方法调用指令为invokestatic,再者他是一个静态解析过程,我们可以从字节码清除地看出来

StaticResolution.say1 ()字样。

say2()是一个final方法,不可以重载,重写,虽然是一个invokevirtual方法但是他也是静态解析的。

say3()也是invokevirtual方法,但也是静态解析的。

say3(int i)是say3()的一个重载方法,但是重载是在编译期就确定到底要调用哪个方法,所以也是静态解析。

分派(Dispatch)

分派有静态分派与动态分派之分。

静态分派:先看一下代码

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,guy!");
	}

	public void <span style="color:#ff0000;">sayHello(Man guy)</span> {
		System.out.println("hello,gentleman!");
	}

	public void <span style="color:#ff0000;">sayHello(Woman guy)</span> {
		System.out.println("hello,lady!");
	}

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

我们注意到两个sayHello方法的参数类型分别是Man和Woman,都继承自Human。

我们看一下main方法的bytecode.

public static main(String[]) : void
   L0
    LINENUMBER 32 L0
    NEW StaticDispatch$Man
    DUP
    INVOKESPECIAL StaticDispatch$Man.<init> () : void
    ASTORE 1
   L1
    LINENUMBER 33 L1
    NEW StaticDispatch$Woman
    DUP
    INVOKESPECIAL StaticDispatch$Woman.<init> () : void
    ASTORE 2
   L2
    LINENUMBER 34 L2
    NEW StaticDispatch
    DUP
    INVOKESPECIAL StaticDispatch.<init> () : void
    ASTORE 3
  <span style="color:#ff0000;"> L3
    LINENUMBER 35 L3
    ALOAD 3: sr
    ALOAD 1: man
    INVOKEVIRTUAL StaticDispatch.sayHello (StaticDispatch$Human) : void
   L4
    LINENUMBER 36 L4
    ALOAD 3: sr
    ALOAD 2: woman
    INVOKEVIRTUAL StaticDispatch.sayHello (StaticDispatch$Human) : void</span>
   L5
    LINENUMBER 37 L5
    RETURN
   L6
    LOCALVARIABLE args String[] L0 L6 0
    LOCALVARIABLE man StaticDispatch$Human L1 L6 1
    LOCALVARIABLE woman StaticDispatch$Human L2 L6 2
    LOCALVARIABLE sr StaticDispatch L3 L6 3
    MAXSTACK = 2
    MAXLOCALS = 4

对于型如Human man=new Man();其中Human我们称之为静态类型,Man我们称之为实际类型。静态类型在编译期就确定了,而实际类型要到运行时才能真正地知道是什么类型。

对于方法重载,由于方法重载是在编译器就确定到底TMD要调用哪个方法,所以重载方法的参数确定,就必须由参数的静态类型(加入参数是对象类型)来确定的。

所以TMD两个sayHello方法都会变成这样

StaticDispatch.sayHello (StaticDispatch$Human)我们注意到为静态分派,且参数的类型变为Human即静态类型。

动态分派

动态分派一般对应Java多态的重写(override)。

我们先看一段代码先。

/**
 * 方法动态分派演示
 * @author cxt
 */
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) {
		<span style="color:#ff0000;">Human man = new Man();
		Human woman = new Woman();
		man.sayHello();
		woman.sayHello();
		man = new Woman();
		man.sayHello();</span>
	}
}

mian方法对应的bytecode位:

public static main(String[]) : void
   L0
    LINENUMBER 28 L0
    NEW DynamicDispatch$Man
    DUP
    INVOKESPECIAL DynamicDispatch$Man.<init> () : void
    ASTORE 1
   L1
    LINENUMBER 29 L1
    NEW DynamicDispatch$Woman
    DUP
    INVOKESPECIAL DynamicDispatch$Woman.<init> () : void
    ASTORE 2
  <span style="color:#ff0000;"> L2
    LINENUMBER 30 L2
    ALOAD 1: man
    INVOKEVIRTUAL DynamicDispatch$Human.sayHello () : void</span>
   <span style="color:#ff0000;">L3
    LINENUMBER 31 L3
    ALOAD 2: woman
    INVOKEVIRTUAL DynamicDispatch$Human.sayHello () : void</span>
   L4
    LINENUMBER 32 L4
    NEW DynamicDispatch$Woman
    DUP
    INVOKESPECIAL DynamicDispatch$Woman.<init> () : void
    ASTORE 1: man
   <span style="color:#ff0000;">L5
    LINENUMBER 33 L5
    ALOAD 1: man
    INVOKEVIRTUAL DynamicDispatch$Human.sayHello () : void</span>
   L6
    LINENUMBER 34 L6
    RETURN
   L7
    LOCALVARIABLE args String[] L0 L7 0
    LOCALVARIABLE man DynamicDispatch$Human L1 L7 1
    LOCALVARIABLE woman DynamicDispatch$Human L2 L7 2
    MAXSTACK = 2
    MAXLOCALS = 3

我们注意到sayHello是一个重写方法,且调用此方法的命令都是invokevirtual DynamicDispatch,也就是动态分派的,动态分派会在运行时,确定对象的实际类型,然后再确定要去调用哪个方法,这是Java最重要的一个特性。

我们看一些 invokevirtual指令的多态执行过程。

invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

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

2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这

个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。

3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

单分派与多分派

/**
 * 单分派、多分派演示
* @author cxt
 */
public class Dispatch {

	static class QQ {}

	static class _360 {}

	public static class Father {
		public void hardChoice(QQ arg) {
			System.out.println("father choose qq");
		}

		public void hardChoice(_360 arg) {
			System.out.println("father choose 360");
		}
	}

	public static class Son extends Father {
		public void hardChoice(QQ arg) {
			System.out.println("son choose qq");
		}

		public void hardChoice(_360 arg) {
			System.out.println("son choose 360");
		}
	}

	public static void main(String[] args) {
		Father father = new Father();
		Father son = new Son();
		father.hardChoice(new _360());
		son.hardChoice(new QQ());
	}
}

运行结果为:

father choose 360
son choose qq

bytecode:

public static main(String[]) : void
   L0
    LINENUMBER 34 L0
    NEW Dispatch$Father
    DUP
    INVOKESPECIAL Dispatch$Father.<init> () : void
    ASTORE 1
   L1
    LINENUMBER 35 L1
    NEW Dispatch$Son
    DUP
    INVOKESPECIAL Dispatch$Son.<init> () : void
    ASTORE 2
   <span style="color:#ff0000;">L2
    LINENUMBER 36 L2
    ALOAD 1: father
    NEW Dispatch$_360
    DUP
    INVOKESPECIAL Dispatch$_360.<init> () : void
    INVOKEVIRTUAL Dispatch$Father.hardChoice (Dispatch$_360) : void</span>
  <span style="color:#ff0000;"> L3
    LINENUMBER 37 L3
    ALOAD 2: son
    NEW Dispatch$QQ
    DUP
    INVOKESPECIAL Dispatch$QQ.<init> () : void
    INVOKEVIRTUAL Dispatch$Father.hardChoice (Dispatch$QQ) : void</span>
   L4
    LINENUMBER 38 L4
    RETURN
   L5
    LOCALVARIABLE args String[] L0 L5 0
    LOCALVARIABLE father Dispatch$Father L1 L5 1
    LOCALVARIABLE son Dispatch$Father L2 L5 2
    MAXSTACK = 3
    MAXLOCALS = 3

fahter.hardChioce(XX)方法到底要调用那个是在编译期间确定的,重载嘛(静态分派),由于有两个选择到底TMD的是QQ还是360或者其他都在编译期间就确定了。因为有多个宗量(可以理解为参数类型的个数)的选择,所以Java是属于一种静态多宗量分派语言。

而在运行时,son.hardChoice(XXXX),唯一确定的就是son到底是什么类型,运行时发现原来是Son类型,所以就会调用Son的hardchoice(XXX),至于方法的参数是什么,JVM就没有在运行时再去确定,因为已经在编译期间已经确定是什么参数了,所以运行时只是考虑到底实际类型是什么,要调用实际类型的哪一个方法。

所以我们可以知道其实Java就是一种动态单宗量分派语言。(据说C#已经支持动态多宗量分派了)。

参考自周志明的《深入理解Java虚拟机》

(完)

时间: 2024-07-30 03:38:47

关于JVM中方法调用的相关指令,以及解析(Resolution)和分派(Dispatch)的解释——重载的实现原理与重写的实现原理的相关文章

java中方法调用

JAVA中方法的调用[基础] 一.调用本类中的方法 方法一.被调用方法声明为static ,可以在其他方法中直接调用.示例代码如下: public class HelloWord { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub String str="HelloWord!"; int a=0; int b=a+1; int result=

关于一个类中方法调用

大家都知道:对象是对客观事物的抽象,类是对对象的抽象,对象是类的实例. 在类中来调用方法,不过调用方法也有种种的情况. 在同一个类中的两个方法相互调用; 1.如果两个方法都是普通方法,那么可以直接用 public void eat(){ System.out.println("内容1"); sleep(); } public void sleep(){ System.out.println("内容2"); } 2.如果两个方法都是静态方法,那么可以直接用 publi

对于JVM中方法区,永久代,元空间以及字符串常量池的迁移和string.intern方法

在Java虚拟机(以下简称JVM)中,类包含其对应的元数据,比如类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已确定的符号引用和虚方法表. 在过去(当自定义类加载器使用不普遍的时候),类几乎是"静态的"并且很少被卸载和回收,因此类也可以被看成"永久的".另外由于类作为JVM实现的一部分,它们不由程序来创建,因为它们也被认为是"非堆"的内存. 在JDK8之前的HotSpot虚拟机中,类的这些"永久的"

Android中Activity与Task相关的属性解析

与Task相关的属性解析 android:allowTaskReparenting 用来标记Activity能否从启动的Task移动到有着affinity的Task(当这个Task进入到前台时)--"true",表示能移动,"false",表示它必须呆在启动时呆在的那个Task里.    如果这个特性没有被设定,设定到<application>元素上的allowTaskReparenting特性的值会应用到Activity上.默认值为"fals

关于一个类中方法调用情况

一.如果两个方法都是普通方法,那么可以直接用方法名调用 public void Happy() { System.out.println("play!") Sleep(); } public void Play() { System.out.println("Sleep!!"); } } } 二.如果两个方法都是静态方法,那么可以直接用方法名调用 public static void Play() { System.out.prinltn("Hello!&

关于 python 类与继承中方法调用 的 一个小知识点记录

(1)D类,多继承于C类与B类,C类与B类继承于A类.C类中没用__init__(), C类的实例化会先寻找第一个继承类是否存在__init__(),也就是B类的__init__().因为python3中使用的是广度优先的方法,寻找路径为D-->B-->C-->A 关于其他方法的继承,也是这个顺序. class A(object): def __init__(self): print("i am A") def call(self): print("A CA

Liunx系统中磁盘分区及相关指令——理论篇

本次博客将初步介绍磁盘的基本构造,以及有关Liunx操作系统中对新添磁盘的设置.分区以及挂载的详细指令理论. 目录: 磁盘基础 规划磁盘分区 创建文件系统 挂载.卸载文件系统 1.磁盘基础 首先,何为磁盘?磁盘(disk)是指利用磁记录技术存储数据的存储器.磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失.早期计算机使用的磁盘是软磁盘(soft disk,简称软盘),如今常用的磁盘是硬磁盘(hard disk,简称硬盘). 1-1 磁盘结构 我们现在所用的硬磁盘

Liunx系统中磁盘分区及相关指令——实验操作篇(理论基于理论篇)

本次博客将详细说明有关Liunx操作系统中对新添磁盘的设置.分区以及挂载的详细指令操作. 目录: 规划磁盘分区 创建文件系统 挂载.卸载文件系统 一.规划磁盘分区 一块新加入的磁盘想要能够正常使用,所谓千里之行始于足下,第一步是非常重要的.那么在Liunx系统中想要让新加的磁盘正常使用,第一步就是要进行磁盘的分区. 1.为服务器添加新的磁盘 打开VM虚拟机(本次实验环境均在VM虚拟机中进行)在保证虚拟机没有开启的情况下,右击"Centos 7-1"(步骤1)选择设置,点击添加(步骤2)

JS中this指向问题相关知识点及解析

概括:this指向在函数定义的时候是无法确定的,只有在函数调用执行的时候才能确定this最终指向了谁,this最终指向的是调用它的对象(常见的说法,后面有小小的纠正): 例1: 图中的函数fn1其实是window对象下面的一个方法,相当于window.fn1()调用了这个方法,而name是fn1的私有变量,所以全局下是没有name这个变量的,所以结果如上所示,this最终指向的也是window这个对象. 例2: 此时this指向的是obj对象,因为fn2是通过obj调用的,所以可以理解this最