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

[目录]
概述

1 概述

  • 不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行通过解释器执行)和编译执行通过即时编译器产生本地代码执行)两种选择也可能两者兼备
  • 从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件处理过程字节码解析的等效过程输出的是执行结果
  • 本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。

2 运行时栈帧结构

  • 栈帧(Stack Frame)是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素,用于支持虚拟机进行方法调用和方法执行的数据结构

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

  • 每一个方法从调用开始至执行完成的过程 **---------> **一个栈帧在虚拟机栈里面从入栈到出栈的过程

  • 编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中

  • 一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态(方法间互相调用)

  • 对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current StackFrame),与这个栈帧相关联的方法称为当前方法(Current Method)

2.1 局部变量表

  • 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
  • 局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位
  • 一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有booleanbytecharshortintfloatreferencereturnAddress种类型
  • reference类型表示对一个对象实例的引用,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息

2.2 操作数栈

  • 操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

2.3 动态连接

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

2.4 方法返回地址

两种返回的方法:
正常完成出口(Normal Method Invocation Completion)异常完成出口(Abrupt Method Invocation Completion)
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息

3 方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

3.1 解析

  • 调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)
  • 第七章讲过:目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这
    种解析能成立的前提是方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的
  • 符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法私有方法两大类

Java虚拟机里面提供了5条方法调用字节码指令

方法名 解释 备注
invokestatic 调用静态方法
invokespecial 调用实例构造器<init>方法、私有方法和父类方法
invokevirtual 调用所有的虚方法
invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic todo todo

只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法私有方法实例构造器父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用


invokestaticinvokespecial指令调用的方法 称为非虚方法,与之相
反,其他方法称为虚方法(除去final方法,后文会提到)

3.2 分派

3.2.1 静态分派

  • 定义: 依赖静态类型来定位方法执行版本的分派动作
  • 静态分派发生在编译阶段
  • 确定静态分派的动作实际上不是由虚拟机来执行的。

示例代码

package study8;

/**
 * Created by haicheng.lhc on 05/04/2017.
 *
 * @author haicheng.lhc
 * @date 2017/04/05
 */
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);
    }
}

结果分析:

我们把上面代码中的“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么.虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的

3.2.2.动态分派

  • (Override)有着很密切的关联

示例代码

package study8;

/**
 * Created by haicheng.lhc on 05/04/2017.
 *
 * @author haicheng.lhc
 * @date 2017/04/05
 */
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();
    }
}

解析
使用javap分析

Compiled from "DynamicDispatch.java"
public class study8.DynamicDispatch extends java.lang.Object
  SourceFile: "DynamicDispatch.java"
  InnerClass:
   #9= #4 of #7; //Woman=class study8/DynamicDispatch$Woman of class study8/DynamicDispatch
   #11= #2 of #7; //Man=class study8/DynamicDispatch$Man of class study8/DynamicDispatch
   abstract #13= #12 of #7; //Human=class study8/DynamicDispatch$Human of class study8/DynamicDispatch
  minor version: 0
  major version: 50
  Constant pool:
const #1 = Method   #8.#22; //  java/lang/Object."<init>":()V
const #2 = class    #23;    //  study8/DynamicDispatch$Man
const #3 = Method   #2.#22; //  study8/DynamicDispatch$Man."<init>":()V
const #4 = class    #24;    //  study8/DynamicDispatch$Woman
const #5 = Method   #4.#22; //  study8/DynamicDispatch$Woman."<init>":()V
const #6 = Method   #12.#25;    //  study8/DynamicDispatch$Human.sayHello:()V
const #7 = class    #26;    //  study8/DynamicDispatch
const #8 = class    #27;    //  java/lang/Object
const #9 = Asciz    Woman;
const #10 = Asciz   InnerClasses;
const #11 = Asciz   Man;
const #12 = class   #28;    //  study8/DynamicDispatch$Human
const #13 = Asciz   Human;
const #14 = Asciz   <init>;
const #15 = Asciz   ()V;
const #16 = Asciz   Code;
const #17 = Asciz   LineNumberTable;
const #18 = Asciz   main;
const #19 = Asciz   ([Ljava/lang/String;)V;
const #20 = Asciz   SourceFile;
const #21 = Asciz   DynamicDispatch.java;
const #22 = NameAndType #14:#15;//  "<init>":()V
const #23 = Asciz   study8/DynamicDispatch$Man;
const #24 = Asciz   study8/DynamicDispatch$Woman;
const #25 = NameAndType #29:#15;//  sayHello:()V
const #26 = Asciz   study8/DynamicDispatch;
const #27 = Asciz   java/lang/Object;
const #28 = Asciz   study8/DynamicDispatch$Human;
const #29 = Asciz   sayHello;

{
public study8.DynamicDispatch();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable:
   line 9: 0
   line 22: 4


public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=3, Args_size=1
   0:   new #2; //class study8/DynamicDispatch$Man
   3:   dup
   4:   invokespecial   #3; //Method study8/DynamicDispatch$Man."<init>":()V
   7:   astore_1
   8:   new #4; //class study8/DynamicDispatch$Woman
   11:  dup
   12:  invokespecial   #5; //Method study8/DynamicDispatch$Woman."<init>":()V
   15:  astore_2
   16:  aload_1
   17:  invokevirtual   #6; //Method study8/DynamicDispatch$Human.sayHello:()V
   20:  aload_2
   21:  invokevirtual   #6; //Method study8/DynamicDispatch$Human.sayHello:()V
   24:  new #4; //class study8/DynamicDispatch$Woman
   27:  dup
   28:  invokespecial   #5; //Method study8/DynamicDispatch$Woman."<init>":()V
   31:  astore_1
   32:  aload_1
   33:  invokevirtual   #6; //Method study8/DynamicDispatch$Human.sayHello:()V
   36:  return
  LineNumberTable:
   line 30: 0
   line 31: 8
   line 32: 16
   line 33: 20
   line 34: 24
   line 35: 32
   line 36: 36


}

虽然17 21 语句完全一样,但是结果却不一样,原因是:从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

3.2.3单分派与多分派

  • 方法的接收者与方法的参数统称为方法的宗量,这个定义最早应该来源于《Java与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种
  • 以Java语言的静态分派属于多分派类型
  • Java语言的动态分派属于单分派类型

3.2.4 虚拟机动态分派的实现

  • 虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。

3.3 动态类型语言支持

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

  • 虚拟机是如何调用方法的内容已经讲解完毕,从本节开始,我们来探讨虚拟机是如何执行方法中的字节码指令的
  • 整个运算过程的中间变量都以操作数栈的出栈、入栈为信息交换途径
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容