JVM指令分析实例三(方法调用、类实例)

1,503 阅读5分钟

本篇为《JVM指令分析实例》的第三篇,相关实例均使用Oracle JDK 1.8编译,并使用javap生成字节码指令清单。

前两篇传送门:

JVM指令分析实例一(常量、局部变量、for循环)

JVM指令分析实例二(算术运算、常量池、控制结构)

方法参数

方法的局部变量表,索引值从0开始,且小于局部变量表的长度。

对于实例方法,JVM会隐式传递一个指向当前实例的引用(this),作为方法的第0个局部变量。因此,应用程序实际传递的参数是从索引值1开始的。

但是,对于类方法,由于不需要传递实例引用。因此,应用程序实际传递的参数是从索引值0开始的。

实例代码

int addTwo(int i, int j) {
	return i + j;
}

static int addTwoStatic(int i, int j) {
	return i + j;
}

字节码指令序列

方法调用

invokevirtual

该指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)。

指令带有一个表示索引的参数,运行时常量池在该索引处的项为某个方法的符号引用(提供类名称、方法名称及方法描述符信息)。

invokestatic

该指令用于调用类方法(static方法)

invokespecial

该指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法

调用实例方法代码

int add12and13() {
	return addTwo(12, 13);
}

字节码指令序列

具体执行流程如下:

常量池

invokevirtual的参数为常量池索引值16,对应于常量池的Methodref常量类型,表示一个方法。Methodref结构主要包括两部分,Class(类或接口)和NameAndType(字段或方法),最终都指向字符串常量类型Utf8。

方法的符号引用最终解析结果为:jvm/specification/se8/chapter3/MethodInvoke.addTwo:(II)I,格式为:类全限定名.方法名:方法描述符。

调用类方法代码

int add12and13() {
	return addTwoStatic(12, 13);
}

字节码指令序列

调用类方法与调用实例方法相比,指令序列主要有两点差异:

  1. 调用类方法不需要传入实例引用this,因此不需要压入栈。
  2. 调用类方法使用指令invokestatic(而不是invokevirtual)。

invokespecial实例代码

class Near {
	int it;
	public int getItNear() {
		return getIt(); // 调用私有方法
	}
	private int getIt() {
		return it;
	}
}

class Far extends Near{
	int getItFar() {
		return super.getItNear(); // 调用父类方法
	}
}

字节码指令序列

jvm.specification.se8.chapter3.Near():// 调用Near父类Object的实例初始化方法
0: aload_0
1: invokespecial #10    // Method java/lang/Object."<init>":()V
4: return

public int getItNear():// 调用getIt()私有方法
0: aload_0
1: invokespecial #18    // Method getIt:()I
4: ireturn

int getItFar():// 调用父类getItNear()方法
0: aload_0
1: invokespecial #16    // Method jvm/specification/se8/chapter3/Near.getItNear:()I
4: ireturn

invokespecial指令用于调用实例初始化方法、私有方法和父类方法。

与普通实例方法类似,使用invokespecial指令调用的方法都需要以this作为首个参数。

使用类实例

实例代码1

Object create() {
	return new Object();
}

字节码指令序列

java.lang.Object create():
0: new           #3 // class java/lang/Object. 创建对象,并将其引用值压入栈顶
3: dup              // 复制栈顶引用值,并将复制值压入栈顶
4: invokespecial #8 // Method java/lang/Object."<init>":()V. 调用实例初始化方法
7: areturn          // 从当前方法返回对象引用

new指令用于创建一个对象。dup指令用于复制栈顶数值,并将复制值压入栈顶。

之所以需要在创建对象之后,再复制一个引用,是因为invokespecial和areturn各需要1个对象引用值。

注意,new指令执行后,并没有完成一个对象实例创建的全部过程,只有执行和完成了实例初始化方法后,实例才算创建完全。

实例代码2

public class MyObj {
	int i;
	MyObj example() {
		MyObj o = new MyObj();
		return silly(o);
	}
	MyObj silly(MyObj o) {
		if (o != null) {
			return o;
		} else {
			return o;
		}
	}
}

字节码指令序列

jvm.specification.se8.chapter3.MyObj example():
 0: new           #1 // class jvm/specification/se8/chapter3/MyObj. 创建MyObj对象并将引用压入栈顶
 3: dup             // 复制栈顶引用值,并将复制值压入栈顶
 4: invokespecial #18 // Method "<init>":()V. 调用MyObj实例初始化方法
 7: astore_1        // 将栈顶引用值存入第2个局部变量(o)
 8: aload_0         // 将第1个局部变量压入栈顶(this)
 9: aload_1         // 将第2个局部变量压入栈顶(o)
10: invokevirtual #19 // Method silly:(Ljvm/specification/se8/chapter3/MyObj;)Ljvm/specification/se8/chapter3/MyObj; 调用实例方法(silly),并将返回结果压入栈顶
13: areturn         // 从当前方法返回对象引用

jvm.specification.se8.chapter3.MyObj silly(jvm.specification.se8.chapter3.MyOb
j):
0: aload_1          // 将第2个局部变量压入栈顶(o)
1: ifnull        6  // 如果栈顶数值为null,则跳转到索引号为6的指令继续执行
4: aload_1          // 将第2个局部变量压入栈顶(o)
5: areturn          // 从当前方法返回对象引用
6: aload_1          // 将第2个局部变量压入栈顶(o)
7: areturn          // 从当前方法返回对象引用

实例代码3

public class InstanceFieldGetSet {
	int i;
	void setIt(int value) {
		i = value;
	}
	int getIt() {
		return i;
	}
}

字节码指令序列

void setIt(int):
0: aload_0          // 将第1个局部变量this压入栈顶
1: iload_1          // 将第2个局部变量value压入栈顶
2: putfield      #18 // Field i:I. 设置this实例的i字段值为value
5: return

int getIt():
0: aload_0          // 将第1个局部变量this压入栈顶
1: getfield      #18 // Field i:I. 获取this实例的字段i的值,并压入栈顶
4: ireturn          // 从当前方法返回int类型结果

类实例的字段使用getfield和putfield指令进行访问。

与方法调用指令的操作数类似,putfield及getfield指令的操作数也不代表该字段在类实例中的偏移量。编译器会为实例的这些字段生成符号引用,并保存在运行时常量池之中。这些运行时常量池项会在执行阶段解析为受引用对象中的真实字段位置。

参考

《Java虚拟机规范》(Java SE 8版)

个人公众号

更多文章,请关注公众号:二进制之路

二进制之路