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

关于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虚拟机》
(完)




















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