JVM执行字节码指令是基于栈的架构,就是说全部的操做数都必须先入栈,而后再根据须要出栈进行操做计算,再把结果进行入栈,这个流程和基于寄存器的架构是有本质区别的,而基于寄存器架构来实现,在不一样的机器上可能会没法作到彻底兼容,这也是Java会选择基于栈的设计的缘由之一。java
咱们思考下,当咱们调用一个方法时,参数是怎么传递的,返回值又是怎么保存的,一个方法调用以后又是如何继续下一个方法调用的呢?调用过程当中确定会存储一些方法的参数和返回值等信息,这些信息存储在哪里呢?segmentfault
JVM系列文章1中咱们提到了,每次调用一个方法就会产生一个栈帧,因此咱们确定能够想到栈帧就存储了全部调用过程当中须要使用到的数据。如今就让咱们深刻的去了解一下Java虚拟机栈中的栈帧吧。数组
当咱们调用一个方法的时候,就会产生一个栈帧,当一个方法调用完成时,它所对应的栈帧将被销毁,不管这种完成是正常的仍是忽然的(抛出一个未捕获的异常)。安全
每一个栈帧中包括局部变量表(Local Variables)、操做数栈(Operand Stack)、动态连接(Dynamic Linking)、方法返回地址(Return Address)和额外的附加信息。架构
在给定的线程当中,永远只有一个栈帧是活动的,因此活动的栈帧又称之为当前栈帧,而其对应的方法则称之为当前方法,定义了当前方法的类则称之为当前类。当一个方法调用结束时,其对应的栈帧也会被丢弃。jvm
局部变量表是以数组的形式存储的,并且当前栈帧的方法所须要分配的最大长度是在编译时就肯定了。局部变量表经过index来寻址,变量从index[0]开始传递。ide
局部变量表的数组中,每个位置能够保存一个32位的数据类型:boolean、byte、char、short、int、float、reference或returnAddress类型的值。而对于64位的数据类型long和double则须要两个位置来存储,可是由于局部变量表是属于线程私有的,因此虽然被分割为2个变量存储,依然不用担忧会出现安全性问题。布局
对于64位的数据类型,假如其占用了数组中的index[n]和index[n+1]两个位置,那么不容许单独访问其中的某一个位置,Java虚拟机规范中规定,若是出现一个64位的数据被单独访问某一部分时,则在类加载机制中的校验阶段就应该抛出异常。post
Java虚拟机在方法调用时使用局部变量进行传递参数。在类方法(static方法)调用中,全部参数都以从局部变量中的index[0]开始进行参数传递。而在实例方法调用上,index[0]固定用来传递方法所属于的对象实例,其他全部参数则在从局部变量表内index[1]的位置开始进行传递。spa
注意:局部变量表中的变量不能够直接使用,如须要使用的话,必须经过相关指令将其加载至操做数栈中做为操做数才能使用
操做数栈,在上下文语义清晰时,也能够称之为操做栈(Operand Stack),是一个后进先出(Last In First Out,LIFO)栈,同局部变量表同样,操做数栈的最大深度也是在编译时就肯定的。
操做数栈在刚被建立时(也就是方法刚被执行的时候)是空的,而后在执行方法的过程当中,经过虚拟机指令将常量/值从局部变量表或字段加载到操做数栈中,而后对其进行操做,并将操做结果压入栈内。
操做数堆栈上的每一个条目均可以保存任何Java虚拟机类型的值,包括long或double类型的值。
注意:咱们必须以适合其类型的方式对操做数堆栈中的值进行操做。例如,不可能将两个int类型的值压入栈后将其视为long类型,也不可能将两个float类型值压入栈内后使用iadd指令将其添加。
每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。
在Class文件中的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用做为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种就称为静态解析。而另一部分则会在每一次运行期间才会转化为直接引用,这部分就称为动态链接。
当一个方法开始执行后,只有两种方式能够退出:一种是遇到方法返回的字节码指令;一种是碰见异常,而且这个异常没有在方法体内获得处理。
若是对当前方法的调用正常完成,则可能会向调用方法返回一个值。当被调用的方法执行其中一个返回指令时,返回指令的选择必须与被返回值的类型相匹配(若是有的话)。
方法正常退出时,当前栈帧经过将调用者的pc程序计数器适当的并跳过当前的调用指令来恢复调用程序的状态,包括它的局部变量表和操做数堆栈。而后继续在调用方法的栈帧来执行后续流程,若是有返回值的话则须要将返回值压入操做数栈。
若是在方法中执行Java虚拟机指令致使Java虚拟机抛出异常,而且该异常没有在方法中处理,那么方法调用会忽然结束,由于异常致使的方法忽然结束永远不会有返回值返回给它的调用者。
这一部分具体要看虚拟机产商是如何实现的,虚拟机规范并无对这部分进行描述。
上面的概念听起来有点抽象,下面咱们就经过一个简单的例子来演示一下方法的执行流程。
package com.zwx.jvm; public class JVMDemo { public static void main(String[] args) { int sum = add(1, 2); print(sum); } public static int add(int a, int b) { a = 3; int result = a + b; return result; } public static void print(int num) { System.out.println(num); } } 复制代码
要想了解Java虚拟机的执行流程,那么咱们必需要对类进行编译,获得字节码文件,执行以下命令
javap -c xxxxxxJVMDemo.class >1.txt 复制代码
将JVMDemo.class生成的字节码指令输出到1.txt文件中,而后打开,看到以下字节码指令:
Compiled from "JVMDemo.java" public class com.zwx.jvm.JVMDemo { public com.zwx.jvm.JVMDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_1 1: iconst_2 2: invokestatic #2 // Method add:(II)I 5: istore_1 6: iload_1 7: invokestatic #3 // Method print:(I)V 10: return public static int add(int, int); Code: 0: iconst_3 1: istore_0 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn public static void print(int); Code: 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iload_0 4: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 7: return } 复制代码
若是是第一次接触可能指令看不太懂,可是大体的类结构仍是很清晰的,咱们先来对用到的字节码指令大体说明一下:
了解了字节码指令的大概意思,接下来就让咱们来演示一下主要的几个执行流程:
八、执行istore_2指令,将当前栈顶元素弹出存入局部变量表index[2]的位置,并再次调用iload_2从局部变量表内将index[2]位置的数据压入操做数栈内
方法的调用大体就是不断的入栈和出栈的过程,上述的过程省略了不少细节,只关注了大体流程便可,实际调用比图中要复杂的多。
咱们知道,Java是一种面向对象语言,支持多态,而多态的体现形式就是方法重载和方法重写,那么Java虚拟机又是如何确认咱们应该调用哪个方法的呢?
首先,咱们来看一下方法的字节码调用指令,在Java中,提供了4种字节码指令来调用方法(jdk1.7以前):
注意:在JDK1.7开始,Java新增了一个指令invokedynamic,这个是为了实现“动态类型语言”而引入的,在这里咱们暂不讨论
在类加载机制中的解析阶段,主要作的事情就是将符号引用转为直接引用,可是,对方法的调用而言,有一个前提,那就是在方法真正运行以前就能够惟一肯定具体要调用哪个方法,并且这个方法在运行期间是不可变的。只有知足这个前提的方法才会在解析阶段直接被替换为直接引用,不然只能等到运行时才能最终肯定。
在Java语言中,知足“编译器可知,运行期不可变”这个前提的方法,被称之为非虚方法。非虚方法在类加载机制中的解析阶段就能够直接将符号引用转化为直接引用。非虚方法有4种:
除了非虚方法以外的非final方法就被称之为虚方法,虚方法须要运行时才能肯定真正调用哪个方法。Java语言规范中明确指出,final方法是一种非虚方法,可是final又属于比较特殊的存在,由于final方法和其余非虚方法调用的字节码指令不同。
知道了虚方法的类型,再结合上面的方法的调用指令,咱们能够知道,虚方法就是经过字节码指令invokestatic和invokespecial调用的,而final方法又是一个例外,final方法是经过字节码指令invokevirtual调用的,可是由于final方法的特性就是不可被重写,没法覆盖,因此必然是惟一的,虽然调用指令不一样,可是依然属于非虚方法的范畴。
先来看一个方法重载的例子:
package com.zwx.jvm.overload; public class OverloadDemo { static class Human { } static class Man extends Human { } static class WoMan extends Human { } public void hello(Human human) { System.out.println("Hi,Human"); } public void hello(Man man) { System.out.println("Hi,Man"); } public void hello(WoMan woMan) { System.out.println("Hi,Women"); } public static void main(String[] args) { OverloadDemo overloadDemo = new OverloadDemo(); Human man = new Man(); Human woman = new WoMan(); overloadDemo.hello(man); overloadDemo.hello(woman); } } 复制代码
输出结果为:
Hi,Human Hi,Human 复制代码
这里,Java虚拟机为何会选择参数为Human的方法来进行调用呢?
在解释这个问题以前,咱们先来介绍一个概念:宗量
方法的接收者(调用者)和方法参数统称为宗量。而最终决定方法的分派就是基于宗量来选择的,故而根据基于多少种宗量来选择方法又能够分为:
知道了方法的分派是基于宗量来进行的,那咱们再回到上面的例子中就很好理解了。
overloadDemo.hello(man); 复制代码
这句代码中overloadDemo表示接收者,man表示参数,而接收者是肯定惟一的,就是overloadDemo实例,因此决定调用哪一个方法的只有参数(包括参数类型和个数和顺序)这一个宗量。咱们再看看参数类型:
Human man = new Man(); 复制代码
这句话中,Human称之为变量的静态类型,而Man则称之为变量的实际类型,而Java虚拟机在确认重载方法时是基于参数的静态类型来做为判断依据的,故而最终实际上无论你右边new的对象是哪一个,调用的都是参数类型为Human的方法。
全部依赖变量的静态类型来定位方法执行的分派动做就称之为静态分派。静态分派最典型的应用就是方法重载。
方法重载在编译期就能肯定方法的惟一,不过虽然如此,可是在有些状况下,这个重载版本不是惟一的,甚至是有点模糊的。产生这个缘由就是由于字面量并不须要定义,因此字面量就没有今天类型,好比咱们直接调用一个方法:xxx.xxx(‘1’),这个字面量1就是模糊的,并无对应静态类型。咱们再来看一个例子:
package com.zwx.jvm.overload; import java.io.Serializable; public class OverloadDemo2 { public static void hello(Object a){ System.out.println("Hello,Object"); } public static void hello(double a){ System.out.println("Hello,double"); } public static void hello(Double a){ System.out.println("Hello,Double"); } public static void hello(float a){ System.out.println("Hello,float"); } public static void hello(long a){ System.out.println("Hello,long"); } public static void hello(int a){ System.out.println("Hello,int"); } public static void hello(Character a){ System.out.println("Hello,Character"); } public static void hello(char a){ System.out.println("Hello,char"); } public static void hello(char ...a){ System.out.println("Hello,chars"); } public static void hello(Serializable a){ System.out.println("Hello,Serializable"); } public static void main(String[] args) { OverloadDemo2.hello('1'); } } 复制代码
这里的输出结果是
Hello,char 复制代码
而后若是把该方法注释掉,就会输出:
Hello,int 复制代码
再把int方法注释掉,那么会依次按照以下顺序进行方法调用输出:
char->int->long->float->double->Character->Serializable->Object->chars 复制代码
能够看到,多参数的优先级最低,之因此会输出Serializable是由于包装类Character实现了Serializable接口,注意示例中double的包装类Double,并不会被执行。
咱们把上面第1个例子修改一下:
package com.zwx.jvm.override; public class OverrideDemo { static class Human { public void hello(Human human) { System.out.println("Hi,Human"); } } static class Man extends Human { @Override public void hello(Human human) { System.out.println("Hi,Man"); } } static class WoMan extends Human { @Override public void hello(Human human) { System.out.println("Hi,Women"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new WoMan(); man.hello(man); man.hello(woman); woman.hello(woman); woman.hello(man); } } 复制代码
输出结果为:
Hi,Man Hi,Man Hi,Women Hi,Women 复制代码
这里静态类型都是Human,可是却输出了两种结果,因此确定不是按照静态类型来分派方法了,而从结果来看应该是按照了调用者的实际类型来进行的判断。
执行javap命令把类转换成字节码:
Compiled from "OverrideDemo.java" public class com.zwx.jvm.override.OverrideDemo { public com.zwx.jvm.override.OverrideDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class com/zwx/jvm/override/OverrideDemo$Man 3: dup 4: invokespecial #3 // Method com/zwx/jvm/override/OverrideDemo$Man."<init>":()V 7: astore_1 8: new #4 // class com/zwx/jvm/override/OverrideDemo$WoMan 11: dup 12: invokespecial #5 // Method com/zwx/jvm/override/OverrideDemo$WoMan."<init>":()V 15: astore_2 16: aload_1 17: aload_1 18: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 21: aload_1 22: aload_2 23: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 26: aload_2 27: aload_2 28: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 31: aload_2 32: aload_1 33: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 36: return } 复制代码
咱们能够发现这里的方法调用使用了指令invokevirtual来调用,由于根据上面的分类能够判断,hello方法均是虚方法。
main方法大概解释一下,
main方法中,第7行(Code列序号)和第15行是分别把Man对象实例和Women对象实例存入局部变量变的index[1]和index[2]两个位置,而后16,17两行,21,22两行,26,27两行,31,32两行分别是把须要用到的方法调用者和参数压入操做数栈,而后调用invokevirtual指令调用方法
。
因此上面最关键的就是invokevirtual指令究竟是如何工做的呢?invokevirtual主要是按照以下步骤进行方法选择的:
上面的方法重写例子中,在运行期间才能根据实际类型来肯定方法的执行版本的分派过程就称之为动态分派。
上面方法重载的第1个示例中,是一个静态分派过程,静态分配过程当中Java虚拟机选择目标方法有两点:
而上面方法重写的例子中,由于方法签名是固定的,也就是参数是固定的,那么就只有一个宗量-静态类型,能最终肯定方法的调用,因此属于动态单分派。
因此能够得出对Java而言:Java是一门静态多分派,动态单分派语言
本文主要介绍了一下Java虚拟机中,方法的执行流程以及方法执行过程当中时,Java虚拟机栈中的内存布局,并从字节码的角度诠释了Java虚拟机是如何针对方法重载和方法重写来作出最终调用方法的选择的。
下一篇,将会介绍Java对象在内存中的布局,以及堆这种做为全部线程共享的的内存区域中具体又是如何存储对象的。