从字节码指令看重写在JVM中的实现
Java是解释执行的。包含动态链接的特性。都给解析或执行期间提供了非常多灵活扩展的空间。面向对象语言的继承、封装和多态的特性,在JVM中是怎样进行编译、解析,以及通过字节码指令怎样确定方法调用的版本号是本文例如以下要探讨的主要内容。全文环绕一个多态的简单举例来看在JVM中是怎样实现的。
先简介几个概念。对于字节码运行模型及字节码指令集的相关概念能够參考之前的一篇介绍http://blog.csdn.net/lijingyao8206/article/details/46562933。
一、方法调用的解析
在Class文件里。方法调用是常量池中的一个符号引用,在载入的解析期或者执行时才干确定直接引用。
以下两种指令是在解析期就能够确定直接引用,调用的相应方法也叫作非虚方法。
1、 invokestatic 主要用于调用静态方法
2、 invokespecial 主要用于调用私有方法,构造器,父类方法。
以下两种是在执行时才干确定直接引用的。可是除了final方法,final方法也能够在解析期确定方法的调用版本号。
1、 invokevirtual 虚方法,不确定调用那一个实现类
2、 invokeinterface 接口方法,执行时才干确定实现接口的对象。
二、动态分派
先回想一下静态和动态分派的概念。
Java中,全部以静态类型来定位方法执行版本号的分派动作,都称为静态分派。事实上也就是重载(Overload)就是一种典型的静态分派,在编译期就能够知道方法调用的实际版本号。相对得。动态分派是须要在执行期才干确定方法的版本号,也就是直接引用。一种典型应用就是重写(OverWrite)。在调用invokevirtual指令时,把常量池中的类方法符号引用解析到直接引用的过程就是重写的过程,执行期依据实际类型确定方法的执行版本号。
三、解释运行
Java的解释运行机制,使jaavc编译的过程涵盖了从程序代码的语法、词法分析,再到AST(抽象语法树)生成线性的字节码指令流的过程。
而解释运行是在JVM内部。基于栈的指令集提供了整个平台的可移植性支撑,所以这也是java运行慢的要因,由于不像编译运行。过程中须要很多其它的出入栈指令,而栈又是内存的一个区块。对内存的频繁訪问减少了性能。
四、重写(OverWrite)举例
以下通过一个简单的样例来看一下重写在JVM中的字节码运行模型。
父类:
package bytecode; /** * Created by yunshen.ljy on 2015/7/27. */ public class Wine { public String drink(int ml) { return "drink " + ml + "ml wine"; } }
子类:
package bytecode; /** * Created by yunshen.ljy on 2015/7/27. */ public class Beer extends Wine{ /** * 重写父类方法,实现多态 */ public String drink(int ml){ return "drink " + ml +"ml beer"; } }
调用:
package bytecode; /** * Created by yunshen.ljy on 2015/7/26. */ public class MethodInvotionTest { public String drink(int ml) { Wine wines = new Beer(); return wines.drink(ml); } }
我们都知道。方法调用返回的结果是“drink XX ml beer”。
可是在编译期,字节码中的指令是无法确定实际调用的方法版本号的。
以下看一下调用方法的字节码结构。
public java.lang.String drink(int); descriptor: (I)Ljava/lang/String; flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=2 0: new #2 // class bytecode/Beer 3: dup 4: invokespecial #3 // Method bytecode/Beer."<init>":()V 7: astore_2 8: aload_2 9: iload_1 10: invokevirtual #4 // Method bytecode/Wine.drink:(I)Ljava/lang/String; 13: areturn LineNumberTable: line 10: 0 line 12: 8
第一条指令new 创建对象。把引用入栈指令。new 指令后面的#4就是前文提到的。对于执行时常量池的一个引用,仅仅是javap命令处理成比較易懂的方式来显示。
接着。后面的dup 指令复制刚放入的引用(操作数栈栈顶的值的复制而且将这个“副本”放到栈顶)。Invokespecial 指令就是之前介绍的非虚方法调用指令,在操作数栈中通过当中的一个引用调用Beer的构造器,初始化对象,让还有一个同样引用指向初始化的对象,然后前一个引用(this)弹出栈。
astore_2把引用保存到局部变量表中的索引2位置中。aload_2 把刚才局部变量表中索引2处的值压入操作栈。
iload_1将int參数,也就是局部变量表中索引1处的值压入操作数栈。
Invokevirtual指令将运行操作数栈中的两个值出栈,运行方法调用。指令后的#4仅仅是常量池中的一个符号引用,仅仅有在运行时。才干确定方法的直接引用。
最后的areturn语句将方法运行结果的值出栈,消除当前的栈帧,假设上层有对于当前方法的继续调用,那么会将调用的方法的栈帧设置成当前栈帧(current stack frame)。
以下通过对照每条指令运行后,局部变量表和操作数栈中的值来加强一下上述的出栈、入栈操作。
首先通过stack=2, locals=3, args_size=2 ,先知道了方法的局部变量表是占用了3个slot,操作数栈的size是2个slot。
而由于我们的方法是实例方法(非类方法),所以隐含的thiskeyword的指令我们先描写叙述到局部变量表的第一个位置。占用一个slot。(以下不再画出常量池的结构,可參照calss文件自行分析)
这里仅仅是个简单的举例。和一些知识的回想。知道JVM的运行引擎和字节码指令的一些概念,会让我们对于程序运行的结果预期更加准确,也更易理解一些java语言版本号的设计模式实现。这里仅仅是抛砖引玉,对于其它几种方法调用的指令。也建议大家尝试进行很多其它的分析。