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


    8.1 概述

      执行引擎,一个逼格很高的名字,就是用来执行java字节码的一段代码,执行代码的代码读起来很拗口。与物理机的执行引擎不同,物理机的执行引擎是建立在cpu 操作系统上的,JVM的执行引擎是需要自己编写的。执行引擎执行java字节码的方式有两种,解释执行和编译执行,编译执行就是把字节码编译成native code让物理机去执行。不同的虚拟机采用的方式是不同的,虚拟机规范对次不做要求,只要求他们有一致的外观,所谓一致的外观指的是我同一个字节码在张三的虚拟机和李四的虚拟机上的执行结果要是 一致的。  

    8.2 运行时栈帧结构

      

          

       每一个线程在JVM的内存区里都有一段自己的线程私有的内存区域,这段区域就是虚拟机栈。线程从执行一个方法开始到一个方法执行完毕对应一个栈帧的入栈到出栈的过程,执行一个方法需要的所有内存、数据都在一个栈帧里。

      具体来说,每一个栈帧包括:局部变量表、操作数栈、动态链接方法返回的地址。

      线程在执行一个方法的时候就要分配一个新的栈帧,栈帧里的局部变量表和操作数栈的大小在class文件的方法表的Code属性表里记录。

      一个方法在执行的时候如果调用了另一个方法,会有新的栈帧被压入相应线程的虚拟机栈里。字节码指令是面向栈的,字节码指令面向的栈是栈顶的栈帧里的操作数栈。

    8.2.1 局部变量表

      局部变量表里存放一个方法里使用到的变量,因为是方法里的变量所以被称为局部变量。居然是一个表,访问里面的内容就可以通过索引来实现。如果方法是一个非static的方法,那么这段方法有一个隐含的参数this指向调用该方法的对象,这个this参数被放在了局部变量表的第0个索引。  

    8.2.2 操作数栈

      字节码指令是面向栈的,所以需要一个栈来支持字节码指令的执行,就是这个操作数栈。操作数栈一开始是空的,随着程序的运行,字节码指令频繁的load store操作数,并对操作数栈里的数据执行操作。

    8.2.3 动态链接

      执行一个方法会创建一个新的栈帧,栈帧里需要一个指向一个具体的方法的引用,持有这个引用是为了实现方法调用过程中的动态链接。

    8.2.4 方法返回地址

      方法执行完毕有两种情况,一种是正常执行,一种是异常。

      正常执行完毕一个方法,需要返回该方法的调用者,这里就涉及到一个现场恢复的问题。一个方法执行完毕后,PC寄存器要修改以便指向其调用者,如果有返回值需要把返回值放到调用者的操作数栈里。

    8.2.5 附加信息

      允许虚拟机实现者自己增加的一些规范里没有的信息

    8.3 方法调用

      java编译的时候没有连接的步骤,所有的引用都是符号引用,需要再加载的时候或者执行的时候翻译成直接引用即具体的入口地址。

    8.3.1 解析

      如果一个符号引用满足“编译期可知,运行期不变”,那么这个符号引用在编译期间就可以判断出其直接引用的地址,那么在class文件加载过程中的解析阶段会翻译成直接引用。从这里可以推测,很多方法在编译的时候是不知道其具体实现方法在哪里,比如一个接口,一个可以被重写的方法,这些方法是不确定的,是可变的。

      “不变”的方法主要是static修饰的方法和private修饰的方法,前者是与类相关的,后者是不可重写的。这两种方法的特点是我代码写好了,编译通过了,他们的地址就确定了(虚拟地址),这种符号引用到直接引用的翻译称为解析,在class文件加载的过程中完成。

      从底层来说,如果java代码在被编译成class的时候,一个方法调用是通过 invokestatic invokespecial来实现的,那么在class文件加载的时候的解析过程就会被翻译成直接引用,这类方法被称为非虚方法。除此之外有一个特例,final修饰的方法虽然使用invokevitural来调用,但是无法被重写,他也是一种非虚方法。  

    8.3.2 分派

      分派是只在有多个可以选择的方法下,选择一个更合适的方法去执行,当然这个选择的过程是由JVM来完成的,包括重载和重写,说白了分派过程实现了重载和重写方法的选择。分派和解析并不是符号引用翻译成直接引用的前后的两个阶段,并不是现在class加载阶段完成解析然后在执行阶段完成分派,这是从两个维度去描述连接的过程。最直接的例子就是static方法也存在重载,那么即在class文件加载的时候完成对static方法的解析,在解析的过程中发现了该方法存在重载,此时还要通过相应的策略完成分配即选择一个最合适的重载的方法。

    1、静态分派

      类Man继承自Human,Human man = new Man() Human称为变量的静态类型,Man称为动态类型。我的理解是一个引用被声明出来的类型是静态类型,这个引用可以指向的类型不仅仅是该引用被声明时的类型,可以是其子类,一个引用具体指向的对象的类型被称为动态类型。

      静态类型和动态类型在声明完成后是可以改变的。动态类型的改变很好理解,一个父类的引用可以指向多个不同的其子类实现,指向不同的实现就改变了不同的类型。静态类型的改变指的是只改变引用的类型而不改变引用指向的对象的类型。

      

      因而无论一个变量的静态类型如何变化,在程序结束的时候一定是能够确定其静态类型的,即在编译的时候就能确定静态类型。那动态类型呢?动态类型只有在程序执行的时候才能确定一个变量的动态类型。这么说很是抽象,如果把一个变量的动态类型理解为一个引用指向的具体的对象,再直白一点就是一个符号引用的直接引用,结合之前的知识,即一个符号引用只有在运行的时候才能知道其直接引用。

      方法的重载涉及到重名方法的选择,如果出现了静态类型相同而实际类型不同的情况,根据变量的静态类型来决定重载的版本,又已知静态类型在编译阶段就可以缺点,所以可以得出方法的重载在编译的阶段由编译器完成。这种根据静态类型选择方法具体版本的分派称为静态分派。

    2、动态分派  

      动态分派从底层是使用invokevirtuall来实现的,这么说其实不准确,应该是某些无法在class文件加载过程中完成符号引用到直接引用翻译的方法即虚方法,要使用invokevitural指令来完成,而invokevitural的执行特点赋予了虚方法动态分派的特点。

      invokevotural指令执行的时候,先找操作数栈顶第一个元素指向对象的实例类型。怎么保证在执行invokevirtual的时候操作数栈顶一定是一个引用,万一是一个基本类型的变量咋办?我觉得这个保证应该是由java编译器来完成的。不管如何,invokevirtuall找到了操作数栈顶的引用指向的堆里的对象的实际类型,记为C,如果在C的class文件里的方法表集合里找到了一个与符号引用所需要的方法符合的方法,那么就检查权限,权限检查通过就返回C对象的该方法的直接引用,这样翻译就完成了。如果在C对象的class文件里没有找到,那么就从下到上去找其父类里是否有符合要求的方法。

      所以从这里可以看到invokevirtuall指令的操作数是在栈里,那么只有在一个方法被执行的时候栈才能被创建,也就是说只有方法在执行的时候invokevirtuall才能找到他的操作数,也只有找到操作数才能完成符号引用到直接引用的转换,这就是运行时动态分派,这就是重写的本质。

    3、单分派与多分派

      分派的时候根据一个宗量称为单分派,反之是多分派。

      首先是静态分派,根据静态类型,既包括调用者的静态类型也包括参数的静态类型。所以在静态分配的时候是多分派。

      其次是动态分配,即执行invokevirtual的时候,在静态分配完毕后已经决定好了方法前面,此时的主要分配的目的是选择方法的接受者,即此时操作数栈顶的那个引用的实际类型。所以动态分配是单分派。  

      

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

      

     

  • 相关阅读:
    从点子到产品
    基础设计模式
    SpringBoot单元测试
    结构化思维
    考试脑科学读书笔记
    爆发式成长的思维
    人人都是产品经理笔记
    文本处理工具awk
    CSP-S2019【绍兴一中集训】
    寒假集训【1.28】
  • 原文地址:https://www.cnblogs.com/AshOfTime/p/10304937.html
Copyright © 2020-2023  润新知