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

8.1 概述

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

8.2 运行时栈帧结构

  

      

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

  具体来讲,每个栈帧包括:局部变量表、操做数栈、动态连接方法返回的地址。操作系统

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

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

8.2.1 局部变量表

  局部变量表里存放一个方法里使用到的变量,由于是方法里的变量因此被称为局部变量。竟然是一个表,访问里面的内容就能够经过索引来实现。若是方法是一个非static的方法,那么这段方法有一个隐含的参数this指向调用该方法的对象,这个this参数被放在了局部变量表的第0个索引。  code

8.2.2 操做数栈

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

8.2.3 动态连接

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

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方法的解析,在解析的过程当中发现了该方法存在重载,此时还要经过相应的策略完成分配即选择一个最合适的重载的方法。

一、静态分派

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

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

  

  于是不管一个变量的静态类型如何变化,在程序结束的时候必定是可以肯定其静态类型的,即在编译的时候就能肯定静态类型。那动态类型呢?动态类型只有在程序执行的时候才能肯定一个变量的动态类型。这么说非常抽象,若是把一个变量的动态类型理解为一个引用指向的具体的对象,再直白一点就是一个符号引用的直接引用,结合以前的知识,即一个符号引用只有在运行的时候才能知道其直接引用。

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

二、动态分派  

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

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

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

三、单分派与多分派

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

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

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

  

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

相关文章
相关标签/搜索