JVM之字节码执行引擎

方法调用:java

方法调用不一样于方法执行,方法调用阶段惟一任务就是肯定被调用方法的版本(即调用哪个方法),暂时还不执行方法内部的具体过程。方法调用有,解析调用,分派调用(有静态分派,动态分派)。架构

方法解析:spa

解析调用必定是一个静态的过程,在编译期就彻底肯定,能够在类加载的解析阶段就把涉及的符号引用转化为直接引用,不会延迟到运行期再去完成。code

全部方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用(前提是,方法在程序真正运行前就能够肯定调用的版本,而且这个方法的调用版本在运行期不会改变,也就是说,调用目标在程序代码写好,编译器编译时就能肯定下来)。这类方法的调用称为解析。对象

编译期可知,运行期不变,这样的方法主要有静态方法和私有方法这两大类,前者与类型关联,后者在外部不可被访问,这两种方法各自的特色决定了它们都不能经过继承或别的方式重写其余版本,所以它们都适合在类加载阶段进行解析。blog

与之对应的是,在Java虚拟机里面提供了5条调用字节码指令,有:继承

invokestatic:调用静态方法接口

invokespecial:调用实例构造器<init>方法,私有方法和父类方法。内存

invokevirtual:调用全部的虚方法ci

invokeinterface:调用接口方法,会在运行时在肯定一个实现此接口的对象。

只要能被invodestatic,invokespecial指令调用的方法,均可以在解析阶段中肯定惟一的调用版本(而invokevirtual,invokeinterface指令调用的方法则不行,由于他们2调用的方法须要去找到子类对应的方法(若是有的话),实现该接口的类的方法,这些都是在运行期肯定的)。能被invokestatic,invokespecial指令对应的方法有静态方法,私有方法,实例构造器方法,父类方法,它们会在类加载的时候(准确说是在解析阶段)就会把符号引用转化为直接引用,这些方法称为非虚方法,与之对应的称为虚方法(final方法除外,即便它是虚方法,可是由于它不能被重写,能够在类加载的解析阶段就把涉及的符号引用转化为直接引用,不会延迟到运行期再去完成)。

分派之静态分派:

分派调用多是静态的,也多是动态的,根据分派的宗量数可分为单分派和多分派。分派调用过程将会揭示多态性特征的一些基本的体现,如重载,重写是怎样实现的。、

变量的类型有静态类型和实际类型之分,因此在加载器没法肯定变量的实际类型,也就没法肯定变量所调用方法的版本。

静态类型(或者称外观类型,C++中称为编译器类型),实际类型(C++中称为运行期类型)。这两个概念是实现多态的基础。

虚拟机(准确说是编译器)在重载时是经过参数的静态类型而不是实际类型做为断定依据的,静态类型是编译期可知的,由于在编译阶段,Javac编译器会根据参数的静态类型决定使用哪一个重载版本。全部依赖静态类型来定位方法执行版本的分派动做称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,所以肯定静态分派的动做实际上不是由虚拟机来执行的。另外,编译器虽然能肯定出方法的重载版本,但在不少状况在这个版本有多个选择,每每只能肯定一个更加合适的。如,基础类型变量,以字符变量为例重载时首选其实际的类型(接触类型没有静态类型这一说),而后是int,long,Character,Serializable,Object.

分派之动态分派:

invokevirtual指令(执行全部的虚方法)的运行时解析过程大体分为如下几个步骤:

1.找到操做数栈顶的第一个元素所指向的对象(调用当前方法的对象)的实际类型,记做C

2.若是在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,若是经过则返回这个方法的直接引用,查找过程结束;若是不经过抛出java.lang.IllegalAccessError异常;

3.不然,按照继承关系从下往上依次对C的父类进行第2步的搜索和校验过程;

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

这个过程就是Java语言中方法重写的本质,在这个过程当中将常量池中的类方法的符号引用转化为直接引用(针对于这些虚方法)。咱们把这种在运行期根据实际类型肯定方法执行版本的分派过程称为动态分派。

单分派与多分派:

方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

对照实例来分析:

 

 1 public class Dispath
 2 {
 3     static class QQ{}
 4 
 5     static class _360{}
 6     
 7     public static class Father {
 8         public void hardChoice(QQ arg){
 9             System.out.println("father choose qq");
10         }
11 
12         public void hardChoice(_360 arg){
13             System.out.println("father choose 360");
14         }
15     }
16 
17     public static class Son extends Father {
18         public void hardChoice(QQ arg){
19             System.out.println("son choose qq");
20         }
21 
22         public void hardChoice(_360 arg){
23             System.out.println("son choose 360");
24         }
25     }
26 
27     public static void main(String[] args){
28         Father father = new Father();
29         Father son = new Son();
30         father.hardChoice(new _360());
31         son.hardChoice(new QQ());
32     }
33 }

 

对于hardChoice()方法

1.编译器选择方法版本的过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一点是静态类型是Father仍是Son,二是方法参数是QQ仍是360。此次选择结果的产物是产生了两条invokevirtual指令,两个指令的参数分别为常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符号引用。如图:

 

 

 

 

由于是根据两个宗量进行选择,因此Java语言的静态分派属于多分派类型。

2.运行阶段的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())”这句代码时,即invokevirtual指令时,因为编译期已经决定目标方法的签名必须是hardChoice(QQ),虚拟机此时不会关心传进来的参数类型了,由于这时的参数的静态类型,实际类型已经对方法的选择不会产生影响了(已经进行了重载,剩下的只有重写了)。惟一产生影响的是方法调用者的实际类型是Father仍是Son。由于只有一个宗量做为选择依据,因此动态分派属于单分派类型。

 

 

  

基于栈的字节码解释执行引擎:

 许多Java虚拟机的执行引擎在执行Java代码时都有解释执行(经过解释器执行),编译执行(经过即时编译器产生本地代码执行)两种选择。

Java编译器输出的指令流,基本上是基于栈的指令集架构,指令流中的指令大部分是零地址指令,他们依赖操做栈进行工做。与之对应的是基于寄存器的指令集,如x86二地址指令集(主流PC机使用)。

基于栈的指令集优势:可移植,代码想对紧凑(一字节对应一个指令),编译器实现更加简单(不用考虑空间分配问题,所需空间在栈上操做)。

缺点:速度慢(由于指令数量多,内存访问频繁,访问栈属于内存访问)。

 

基于栈的解释器执行过程跟处理器执行方式类似。

相关文章
相关标签/搜索