赞
踩
执行引擎是Java虚拟机核心的组成部分之一。物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约,定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式
Java虚拟机通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备。但是从外观上,所有的Java虚拟机的执行引擎输入和输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
Java虚拟机以方法作为最基本的执行单位,**栈桢(Stack Frame)**则是用于支持虚拟机进行方法的调用和方法执行背后的数据结构。每一个方法从调用开始至执行结束的过程,都对应着一个栈桢在虚拟机中从入栈到出栈的过程。对于执行引擎来说,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈桢才是生效的。
每一个栈桢都包括局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。同时在编译Java程序源码的时候,栈桢需要多大的局部变量表和多深的操作数栈已经被分析计算出来,并写到方法表的Code属性中。
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。方法表的Code属性的max_locals数据项定义了该方法局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)作为最小单位。boolean、byte、char、short、int、float 、reference或者returnAddress都是使用一个变量槽存放。long、double使用两个连续的变量槽存放,虚拟机不允许采用任何方式单独访问其中一个,如果出现这种情况会抛出异常。
其中引用reference有两个作用:
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。
如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以使用this关键字来访问这个隐藏的参数。
局部变量表在使用的过程中有两个需要注意的地方:
1、为了节省空间,局部变量表中的变量槽是可以重用的
方法体中定义的变量,其作用域并一定是覆盖整个方法的,如果当前字节码PC计数器的值已经超过了其中某个变量的作用域,那么这个变量对应的变量槽就可以交给其他变量重用。但是需要注意的是重用不是立即发生,下面使用一个例子进行解释,运行时加上-verbose:gc
来显示GC过程:
public class VariableTableTest {
public static void main(String[] args) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.gc();
}
}
结果如下,可以看出placeHolder没有被回收,因为System.gc()
在placeHolder引用变量的作用域中,这很好理解。
再看看下面这段代码:
public class VariableTableTest {
public static void main(String[] args) {
{
byte[] placeHolder = new byte[64 * 1024 * 1024];
}
// int i = 0;
System.gc();
}
}
结果如上图所示,按道理说placeHolder作用域已经过了,为什么还是没有被回收呢?先别急,咱们先看看下面这个代码:
public class VariableTableTest {
public static void main(String[] args) {
{
byte[] placeHolder = new byte[64 * 1024 * 1024];
}
int i = 0;
System.gc();
}
}
这个代码中仅仅是增加了int i = 0
这个操作,从GC结果就可以看出placeHolder引用的变量被回收了。这是为什么呢?这是因为第一次修改时,代码虽然已经离开了placeHolder的作用域,但是在此之后,在没有发生过任何对局部变量表的读写操作,placeHolder原本占用的变量槽还没有被其他变量所复用,所以GC Roots一部分的局部变量表仍然保持对他的关联。所以说变量槽的复用不是及时。
2、局部变量必须赋初值之后才能使用
对于类字段来时有两次赋初始值的过程,一次在准备阶段,赋予系统默认值;另一次在初始化阶段,赋予程序定义的初始值。而对于局部变量,并没有赋予系统默认值这个过程,所以局部变量如果没有赋初值是不能使用的,编译期就会报错。
操作数栈(Operand Stack)也常被称为操作栈,它是一个先进先出(LIFO)的栈。操作数栈的最大深度在编译的时候写入到code属性中max_stacks数据项中。
方法刚开始执行时,操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈和出栈操作。
操作数栈中元素的数据类型必须字节码指令的序列严格匹配。
大多数虚拟机在实现的时候都会对栈桢做一定的优化,让下面栈桢的部分操作数栈与上面部分的局部变量表重叠在一起,如下图所示。这样做的好处有:一、节约空间,二、方法调用时可以直接共用一部分数据,无须进行额外的参数复制转递。
每个栈桢中都包含一个指向运行时常量池中该栈桢所述方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
当一个方法开始执行之后,只有两种方式退出这个方法
方法退出的过程实际上相当于把当前栈桢出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈桢的操作数栈中,调整PC计数器的以指向方法调用指令的后一条指令。
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈桢中,例如与调试、性能收集相关的信息。
方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还没涉及方法内部的执行过程。
Class文件的编译过程中不包含传统程序语言编译的连接过程,一切方法调用在Class文件中都是符号引用,而不是方法在实际运行时内存布局的入口地址(也就是直接引用)。这样做虽然增加了复杂度,但是给Java带来了更强大的动态扩展能力。
调用不同类型的方法,字节码指令集中设计了不同的指令
<init>()
方法、私有方法和父类中的方法所有方法调用的目标方法在Class文件中都只是一个常量池中的引用符号,在类的解析阶段,会将其中一部分符号引用转化为直接引用,前提是:方法在程序真正运行之前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期间时不可改变的。换句话说,调用目标在程序代码写好,编译器进行编译那一刻就已经确定下来了。这来方法的调用称为解析(Resolution)
上面提到的5种调用指令中,只要被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,Java语言中符合这个要求的有静态方法、私有方法、实例构造器、父类方法这4种。除此之外还有final方法,虽然final方法使用invokevirtual调用,但是它无法被覆盖,没有其他版本的可能。
解析调用一定是个静态过程,在编译期间就完全确定,在类加载的解析阶段就会把设计符号引用转化为明确的直接引用
示例:将下面的StaticResolution编译得到Class,使用javap -verbose StaticResolution.class
查看字节码文件的内容,可以发现,invokestatic后面的方法已经被解析出来了
public class StaticResolution {
public static void sayHello(){
System.out.println("Hello world!");
}
public static void main(String[] args) {
StaticResolution.sayHello();
}
}
1、静态分派
所有依赖静态类型来决定方法执行版本的分派动作都成为静态分派。静态分派最典型的应用是方法重载(overload)。下面使用一个例子来解释上面的定义:
public class StaticDispatch { static abstract class Human{} static class Man extends Human {} static class Woman extends Human {} public void sayHello(Human human){ System.out.println("Hello guy!"); } public void sayHello(Man man){ System.out.println("Hello guy!"); } public void sayHello(Woman woman){ System.out.println("Hello guy!"); } public static void main(String[] args) { Human man = new Man(); Woman woman = new Woman(); StaticDispatch sd = new StaticDispatch(); sd.sayHello(man); sd.sayHello(woman); } }
上面代码的结果是:
在解释为什么之前,先介绍两个关键概念,对于Human man = new Man()
这段代码中
Human
称为变量的静态类型(Static Type),或者称为外观类型(Apparent Type)Man
称为变量的实际类型(Actual Type),或者称为运行时类型(Runtime Type)变量本身的静态类型不会改变,并且最终的静态类型在编译期是可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型
虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据,由于静态类型在编译器可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本
对于字面零类型(char)重载时的方法匹配优先级从高到低依次是:
2、动态分派
在运行期间根据实际类型确定方法执行的分派过程称为动态分派。动态分派与多态性的另外一个体现**重写(override)**模切相关
我们使用下面的代码进行讲解:
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) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
运行结果如上图所示,上面的这段代码就是多态的体现,那它在虚拟机内部是怎么实现的呢,咱们来看看上面这段代码的字节码:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class jvm/ch8/DynamicDispatch$Man 3: dup 4: invokespecial #3 // Method jvm/ch8/DynamicDispatch$Man."<init>":()V 7: astore_1 8: new #4 // class jvm/ch8/DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method jvm/ch8/DynamicDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method jvm/ch8/DynamicDispatch$Human.sayHello:()V 20: aload_2 21: invokevirtual #7 // Method jvm/ch8/DynamicDispatch$Human.sayHello:()V 24: new #4 // class jvm/ch8/DynamicDispatch$Woman 27: dup 28: invokespecial #5 // Method jvm/ch8/DynamicDispatch$Woman."<init>":()V 31: astore_1 32: aload_1 33: invokevirtual #6 // Method jvm/ch8/DynamicDispatch$Human.sayHello:()V 36: return
0~15行的字节码是准备工作,作用是建立man和woman的内存空间,调用Man和Woman类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表的变量槽中,这些动作实际对应了Java源码中的这两行代码:
Human man = new Man();
Woman woman = new Woman();
接下来的16~21行是关键部分,16行和20行的load指令分别将刚刚创建的两个对象引用压入栈顶,这两个对象是将要执行的sayHello()方法的所有者,第17和21使用invokevirtual指令对方法进行了调用,invokevirtual指令的运行时解析过程为:
正是因为invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以动态分派是根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。
同时对于字段来说,当子类中声明了与父类同名的字段时,虽然子类的存中两个字段都存在,但是子类的字段会掩蔽父类的同名字段。调用字段时是根据静态类型进行判断调用哪个变量
3、单分派和多分派
方法的接收者和方法的参数统称为综量。根据分派基于多少个综量,可以将分派划分为单分派和多分派两种。在Java语言中,静态分派属于多分派,动态分派属于单分派
4、虚拟机动态分派实现
动态分派是非常频繁的操作,为了提高效率,虚拟机会为类型在方法区建立一个虚方法表(Virtual Method Table,也成为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表-Interface Method Table,简称itable )使用虚方法表索引来代替元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表的地址会被替换为指向子类实现版本的入口地址。
待补充
上面介绍完了Java虚拟机是如何调用方法、进行版本选择的。现在开始分析概念模型下的Java虚拟机解释执行字节码时,其执行引擎是如何工作的。大部分的代码程序转换成物理机的目标代码或虚拟机能执行的指令集之前都需要经过下图所示的各个步骤:
图中下面的那条分支是传统编译原理中程序代码代码到目标机器码的生成过程。而中间那条分支就是解释执行的过程。
在Java语言中Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成现行的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。
Javac编译器输出的字节码指令流,基本上是一种基于栈的指令集框架(Instruction Set Architecture,ISA),字节码指令流里边的指令大部分都是零地址操作,他们是依赖操作数栈进行的工作的。与之对应的是基于寄存器的指令集,最典型的就是x86二地址指令集,它是主流PC机中物理硬件直接支持的指令集框架,这些指令依赖寄存器进行工作。下面使用如何计算1+1
对这两种指令集分别举一个例子:
1、基于栈的指令集:
iconst_1
iconst_1
iadd
istore_0
两条iconst_1
指令连续将两个常量1放入操作数栈中,iadd
指令把栈顶的两个值出栈、相加,然后把结果放回栈顶,最后istore_0
指令把栈顶的值放到局部变量表的第0个变量槽中。
基于栈的指令集中的指令是不带参数的,使用操作数栈中的数据作为指令的运算输入,指令的运算结果也存储在操作数栈中。
2、基于寄存器的指令集
mov eax, 1
add eax, 1
mov
指令把EAX寄存器的指设为1,然后add
指令再把这个值加1,结果就保存在EAX寄存器中。这种指令中的每条指令都包含两个单独的输入参数,依赖于寄存器来访问和存储信息。
3、基于栈的指令集与基于寄存器的指令集对比的优缺点
优点:
缺点:
使用下面Java代码对基于栈的解释器执行过程进行分析:
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
它对应的字节码指令为:
public int calc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: bipush 100 2: istore_1 3: sipush 200 6: istore_2 7: sipush 300 10: istore_3 11: iload_1 12: iload_2 13: iadd 14: iload_3 15: imul 16: ireturn
码指令为:
public int calc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: bipush 100 2: istore_1 3: sipush 200 6: istore_2 7: sipush 300 10: istore_3 11: iload_1 12: iload_2 13: iadd 14: iload_3 15: imul 16: ireturn
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。