您的位置:首页 > 编程语言 > Java开发

第8章 虚拟机字节码执行引擎

2018-03-20 09:36 204 查看

8.1 概述

执行引擎是Java虚拟机核心组成部分之一。

在不同虚拟机实现里,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能兼备,可能还会包含几个不同级别的编译器执行引擎。

8.2 运行时栈帧结构

栈帧:

用于支持虚拟机进行方法调用和方法执行的数据结构。是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每个方法从调用开始到完成,对应一个栈帧在虚拟机栈中入栈到出栈。

编译时就已经确定了栈的大小。



8.2.1 局部变量表

是一组变量值存储空间,存放方法参数和方法内部定义的局部变量。

程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定该方法所需分配的局部变量表的最大容量。

单位是变量槽(Slot),没说明确每个槽大小,但每个slot都能存放一个基本数据类型、reference或returnAddress类型的数据。这些都可以用32位或更小的内存来存放。long和double是64位,所以slot空间分配连续的两个。

reference类型:

这个引用至少能做到两点:一是从该引用中直接或间接查找到对象在堆中数据存放的起始地址索引,二是引用中直接或间接查到对象所属数据类型在方法区中存储的类型信息。

8.2.2 操作数栈

也称操作栈。栈的每个元素可以是任意的Java数据类型,包括long和double。

32位数据所占的栈容量是1,64位数据所占的栈容量是2。

方法刚开始执行的时候,操作数栈是空的,执行过程中,会有各种字节码指令在栈中有出入栈操作。

iadd指令为例,这个用于整数型加法,执行时,最接近栈顶的两个元素数据类型必须是int。

栈帧作为虚拟机栈的元素,是完全相互独立的。但是实现中栈帧会有重叠。



这样在方法调用时刻共用一部分数据,无需额外参数复制传递。

8.2.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

8.3 方法调用

方法调用不等于方法执行,该阶段唯一任务就是确定该调用哪一个方法。

Class文件编译过程不包含连接,一切方法调用在Class文件存储的只是符号引用,而不是实际运行时内存中的入口地址(直接引用)。 可以给Java有强大的动态扩展能力。

8.3.1 解析

所有方法调用中的目标方法在Class中都是一个常量池的符号引用

类加载的解析阶段,将其中一部分符号引用转化为直接引用。

调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法调用称为解析

适合在类加载阶段进行解析的方法:静态方法和私有方法。符合“编译器可知,运行期不可变”的要求。

虚拟机5条方法调用字节码指令

1. invokestatic:调用静态方法

2. invokespecial:调用实例构造器方法、私有方法、父类方法

3. invokevirtual:调用所有虚方法

4. invokeinterface:调用接口方法,运行时再确定一个实现此类接口的对象

5. invokedynamic

能被invokestatic和invokespecial指令调用的方法,都可在解析阶段确定唯一版本,有静态方法、私有方法、实例构造器、父类方法4类,在类加载时候会把符号引用解析为该方法的直接引用。这些方法是非虚方法。

非虚方法还有是被final修饰的方法。

8.3.2 分派

静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。

虚拟机中重载和重写是依照此来实现的。

静态分派

public class StaticDispatch {

static abstract class Human {
}

static class Man extends Human {
}

static class Woman extends Human{
}

public void sayHello(Human guy){
System.out.println("hello.guy");
}

public void sayHello(Man guy){
System.out.println("hello.gentleman");
}

public void sayHello(Woman guy){
System.out.println("hello.lady");
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();

StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}

hello.guy
hello.guy


Human man = new Man();

Human称为变量的静态类型,后面的Man是变量的实际类型。静态类型编译器就可知;实际类型变化的结果在运行期才可知。

//实际类型变化
Human man = new Man();
man = new Woman();
//静态类型变化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);


虚拟机重载时通过参数的静态类型而不是实际类型作为判定

所有依赖静态类型定位方法执行版本的分派动作叫静态分派。

静态分派典型应用是方法重载。静态分派发生在编译阶段。

public class Overload {
public static void sayHello(Object arg){
System.out.println("hello Object");
}

public static void sayHello(int arg){
System.out.println("hello int");
}

public static void sayHello(long arg){
System.out.println("hello long");
}

public static void sayHello(Character arg){
System.out.println("hello Character");
}

public static void sayHello(char arg){
System.out.println("hello char");
}

public static void sayHello(char... arg){
System.out.println("hello char...");
}

public static void sayHello(Serializable arg){
System.out.println("hello Serializable");
}

public static void main(String[] args){
sayHello('a');
}
}


分配一个字符’a’,首先是char的方法,然后事int,a除了是字符串,还是数字Unicode的97,后来是long类型是97L。

按照char-i
bc66
nt-long-float-double顺序转型的。因为byte和short类型不安全,所以不能转。

之后会自动装箱,所以是Character,因为Character实现了Serializable接口,注掉Character,就会找到相应序列化接口。可变长度的优先级是最低的。

动态分派

动态分派是多态性的另一个重要体现–重写。

public class StaticDispatch {

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();
}
}


静态类型同样都是Human,导致行为不同的原因是,两个变量的实际类型不同了。

把这种在运行期根据实际类型确定方法执行版本的过程叫动态分派。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  JVM Java