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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 代码编译的结果从机器码转变为字节码,是存储格式的一小步,确实编程语言发展的一大步。正是因为有了字节码这一中间格式才有了Java语言跨平台的特性。 字节码并不能直接基于物理机执行引擎执行,因为物理机执行引擎是建立在特定的处理器,指令集以及操作系统之上的,并不具备跨平台特性。

代码编译的结果从机器码转变为字节码,是存储格式的一小步,确实编程语言发展的一大步。正是因为有了字节码这一中间格式才有了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虚拟机在动态分派过程中并不是通过上述查找过程实现的,而是通过虚方法表实现的。

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

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

相关文章
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
22 3
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
129 0
|
4月前
|
存储 缓存 自然语言处理
(三)JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现
执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。
|
5月前
|
存储 Java 编译器
JVM系列7-虚拟机字节码执行引擎
JVM系列7-虚拟机字节码执行引擎
28 1
|
6月前
|
Java 索引
【JVM】字节码文件的组成部分
【JVM】字节码文件的组成部分
52 1
|
6月前
|
存储 Arthas Java
【JVM系列笔记】字节码
本文介绍了Java虚拟机(JVM)的组成,包括类加载子系统、运行时数据区、执行引擎和本地接口。字节码文件由基础信息(如魔数和版本号)、常量池、字段、方法和属性组成。常量池用于存储字符串等共享信息,方法区则包含字节码指令。执行引擎包含解释器、即时编译器和垃圾回收器,负责字节码的解释和优化。文章还提到了字节码工具,如javap、jclasslib和Arthas,用于查看和分析字节码。
78 0
【JVM系列笔记】字节码
|
6月前
|
存储 Java 索引
深入浅出JVM(十)之字节码指令(下篇)
深入浅出JVM(十)之字节码指令(下篇)
|
6月前
|
存储 Java 索引
深入浅出JVM(九)之字节码指令(上篇)
深入浅出JVM(九)之字节码指令(上篇)
|
17天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
29 4
|
17天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
38 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS