JVM学习笔记(四)——字节码执行引擎

代码编译的结果从机器码转变为字节码,是存储格式的一小步,确实编程语言发展的一大步。正是因为有了字节码这一中间格式才有了Java语言跨平台的特性。

字节码并不能直接基于物理机执行引擎执行,因为物理机执行引擎是建立在特定的处理器,指令集以及操作系统之上的,并不具备跨平台特性。所以执行字节码的责任就交给了虚拟机中的字节码执行引擎。

1 运行时栈帧结构

栈帧是用于刻画Java程序运行时一个方法的调用、执行以及返回过程的数据结构。通过学习前面的博客我们知道Java程序运行时有一块区域叫做虚拟机栈,而虚拟机栈中的元素就是栈帧。一个方法从调用到返回的过程就是一个栈帧从入栈到出栈的过程。

一个栈帧主要由以下4部分构成:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 返回地址

1.1 局部表量表

局部变量表是一个变量值存储空间,用于存储方法参数以及方法内部局部变量。局部变量表的基本存储单位为slot,一个slot可以存放一个int、byte,char,boolean,reference等基本数据结构。

当执行一个方法时,虚拟机使用局部变量表完成从形参到实参的转变过程。如果执行的是实例方法,则局部变量表的0号slot用于存储方法所属实例对象的索引,即this。其余的方法参数则按照顺序从第1号slot开始存储;如果执行的是类方法,方法参数从第0号slot开始存储。

1.2 操作数栈

操作数栈用于存储字节码执行过程中的操作数。当一个方法刚开始执行时,其操作数栈是空的,方法在执行的过程中会有各种字节码指令往操作数栈中写入或读取操作数。举例来说,当执行一个整数加法的指令iadd时,执行引擎会将操作数栈栈顶的两个元素取出(出栈),相加获得结果后再压入栈。

1.3 动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

1.4 返回地址

当一个方法执行完成后有两种返回方式:

  • 正常返回:执行引擎执行到任意一个返回的字节码指令
  • 异常返回:在方法执行过程中遇到异常而退出。异常包括虚拟机内部异常以及代码中使用athrow字节码抛出的异常

无论以何种方式返回,方法退出前都需要回到方法被调用的位置。一般来说方法正常退出时,调用者PC计数器的值即为返回地址;异常退出时,返回地址通过异常处理器来确定。

2 方法调用

方法调用不是方法执行,方法调用的唯一任务就是确定方法执行的版本,并不涉及具体方法的执行。Class文件在编译的过程中并不包含传统编译中的连接,一切方法调用在编译期间只是符号引用而不是方法在实际执行时的内存地址入口。方法调用主要分为两种:

  • 解析调用
  • 分派调用

2.1 解析

所有方法调用的目标方法在Class文件中都只是一个符号引用,在类加载的过程中会将其中一部分符号引用转换成直接引用,能够转换的前提是:该方法在编译时即可确定其调用的版本,且该方法在运行期间是不会改变的。上述解析过程称为静态解析,而与之相对应的就是动态解析。符合静态解析标准的方法主要有以下几种:

  • 私有方法
  • 静态方法
  • 父类方法
  • 被final修饰的方法

可以看出,上述几种方法都是不支持覆写的,所以在编译期即可确认其执行版本,因而支持静态解析。

Java虚拟机一共提供了5个方法调用的指令:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器方法,私有方法和父类方法
  • invokevirtual:调用虚方法
  • invokeinterface: 调用接口方法,会在运行时再确定一个实现此接口的对象
  • invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

invokestatic,invokespecial两个指令所调用的方法都是在编译期即可确定其唯一调用版本,符合这个条件的有静态方法,私有方法,实例构造器和父类方法四类。它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以统称为非虚方法,与之相反,其它方法就称为虚方法(除去final方法)。

Java中的非虚方法除了使用invokestatic与invokespecial指令调用的方法之后还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其它版本,所以也无须对方法接收都进行多态选择,又或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。

静态调用一定是个静态过程,在编译期完全确定。

2.2 分派调用

与解析调用不同的是,分派调用既有静态分派也有动态分派。

2.2.1 静态分派

静态分派多发生在方法的重载上,来看下下面这个例子:

package com.xtayfjpk.jvm.chapter8;  

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 man) {
        System.out.println("hello man...");
    }
    public void sayHello(Woman woman) {
        System.out.println("hello woman...");
    }  

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);
        sd.sayHello(woman);
    }
}

执行结果为:

hello guy...
hello guy...

为什么虚拟机执行的是public void sayHello(Human guy)呢?这里需要解释一个概念,首先来看下main方法中的前两行代码:

Human man = new Man();
Human woman = new Woman();

一个实例对象有静态类型和实际类型两个类型,静态类型在编译时即确定而实际类型则需要到运行时才可确定。上述两个变量的静态类型均为Human,而实际类型则为ManWoman

静态类型在编译时即可确定并不是说静态类型不可改变,下面两行代码即可改变静态类型:

sd.sayHello((Man)man);
sd.sayHello((Woman)woman);

由于虚拟机在编译重载方法调用指令时是通过参数的静态类型进行选择的,并且静态类型是在编译期即可确定的,所以在上述的例子中虚拟机执行的方法是public void sayHello(Human guy)

2.2.2 动态分派

与静态分派相对应的便是动态分派,动态分派的含义也较容易理解,即在运行时才确定方法执行的具体版本并进行分派。动态分派最典型的场景就是——方法重写。

package com.xtayfjpk.jvm.chapter8;  

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

运行结果:

man say hello
woman say hello
woman say hello

在上述代码中,两个静态类型均为Human的对象调用相同的方法却实际上并没有执行相同的方法,说明其方法的分派并不是通过静态类型来确定,而是根据两个变量的实际类型来确定的。Java虚拟机是如何利用实际类型来分派方法的执行版本的呢?来看看上述代码的字节码:

public static void main(java.lang.String[]);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
       0: new           #16                 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Man
       3: dup
       4: invokespecial #18                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Man."<init>":()V
       7: astore_1
       8: new           #19                 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman
      11: dup
      12: invokespecial #21                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #22                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
      20: aload_2
      21: invokevirtual #22                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
      24: new           #19                 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman
      27: dup
      28: invokespecial #21                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman."<init>":()V
      31: astore_1
      32: aload_1
      33: invokevirtual #22                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
      36: return

第16-21行是关键部分,第16和第20两行分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将执行的sayHello()方法的所有者,称为接收者(Receiver),第17和第21两行是方法调用指令,单从字节码的角度来看,这两条调用指令无论是指令(都是invokevirtual)还是参数(都是常量池中Human.sayHello()的符号引用)都完全一样,但是这两条指令最终执行的目标方法并不相同,其原因需要从invokevirutal指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下步骤:

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

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

2.2.3 动态分派实现

由于动态分派在Java程序运行过程中经常会出现,所以通常Java虚拟机在动态分派过程中并不是通过上述查找过程实现的,而是通过虚方法表实现的。

虚拟机为每个类构建了一个方法表,方法表中的每一项存放对应方法的实际入口地址。如果某个方法在子类中没有实现,则子类虚方法表中的该方法指向的是父类的该方法;相反则指向子类的该方法。因而动态分派的过程实际上就是查找虚方法表的过程。

另外为了实现上的方便,具有相同签名的方法,在父类,子类的虚方法表中应该具有一样的索引号,这样当类型转换时,仅需变更查找的方法表即。

时间: 2024-07-29 17:27:23

JVM学习笔记(四)——字节码执行引擎的相关文章

JVM学习笔记(四)------内存调优

首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM. 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量.特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC

JVM学习笔记(四)------内存调优【转】

转自:http://blog.csdn.net/cutesource/article/details/5907418 版权声明:本文为博主原创文章,未经博主允许不得转载. 首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM. 对JVM内存的系统级的调优主要的目的是减少GC的频率和Fu

kvm虚拟化学习笔记(四)之kvm虚拟机日常管理与配置

原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://koumm.blog.51cto.com/703525/1290269 KVM虚拟化学习笔记系列文章列表 ---------------------------------------- kvm虚拟化学习笔记(一)之kvm虚拟化环境安装http://koumm.blog.51cto.com/703525/1288795 kvm虚拟化学习笔记(二)之linux kvm虚拟机安装 h

JVM学习笔记(二)------Java代码编译和执行的整个过程

Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: Java代码编译和执行的整个过程包含了以下三个重要的机制: ● Java源码编译机制 ● 类加载机制 ● 类执行机制 Java源码编译机制 Java 源码编译由以下三个过程组成: ● 分析和输入到符号表 ● 注解处理 ● 语义分析和生成class文件 流程图如下所示: 最后生成的class文件由以下部分组成: ● 结构信息.包括class文件格式版本号及各部分的数量与大

JVM学习笔记(二)------Java代码编译和执行的整个过程【转】

转自:http://blog.csdn.net/cutesource/article/details/5904542 版权声明:本文为博主原创文章,未经博主允许不得转载. Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:   Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码编译机制 类加载机制 类执行机制 Java源码编译机制 Java 源码编译由以下三个过程组成: 分析和输入到符号表 注解处理

Sqlite学习笔记(四)&amp;&amp;SQLite-WAL原理(转)

Sqlite学习笔记(三)&&WAL性能测试中列出了几种典型场景下WAL的性能数据,了解到WAL确实有性能优势,这篇文章将会详细分析WAL的原理,做到知其然,更要知其所以然. WAL是什么       WAL(Write ahead logging)是一种日志模式,它是一种思想,普遍应用于关系型数据库.每个事务执行变更时,修改数据页,同时会产生日志,这样在事务提交后,不需要将修改的脏页刷盘,只需要将事务产生的日志落盘即可返回.WAL保证日志一定先于对应的脏页落盘,就是所谓的WAL.SQLI

JVM学习笔记(三)——虚拟机类加载机制

在介绍完class文件格式后,我们来看下虚拟机是如何把一个由class文件描述的类加载到内存中的.具体来说java中类的加载涉及7个阶段:加载.校验.准备.解析.初始化.使用.卸载. 1.加载时机 并不是所有的类在程序启动时即被加载,为提升效率,虚拟机通常秉承的是按需加载的原则,即需要使用到相应的类时才加载对应的类.具体包括如下几个加载时机: 遇到new.getstatic.putstatic.invokestatic这4条指令时,如果对应的类没有被加载,虚拟机会首先加载对应的类.这4条指令对应

JVM学习笔记(二)——Class文件结构

Class文件是Java程序跨平台的保证,正是由于有了Class文件架起源码和机器码之间的中间桥梁,JVM虚拟机才可以在各种平台上按照统一的规范标准加载Java代码. 作为"写给虚拟机看的"Java代码,Class文件结构必须设计得足够完善,同时由于Java虚拟机规范并不只针对Java,Class文件又不能引入过多细节.本篇博客我们就来介绍下Class文件的结构. 一个Class文件对应一个Java Class,所以一个Class文件记录着一个类的全部信息,JVM通过Class文件将对

Caliburn.Micro学习笔记(四)----IHandle&lt;T&gt;实现多语言功能

Caliburn.Micro学习笔记目录 说一下IHandle<T>实现多语言功能 因为Caliburn.Micro是基于MvvM的UI与codebehind分离, binding可以是双向的所以我们想动态的实现多语言切换很是方便今天我做一个小demo给大家提供一个思路 先看一下效果                                           点击英文  变成英文状态点chinese就会变成中文                         源码的下载地址在文章的最下