小仙女讲JVM(6)—字节码执行引擎

2024-03-11 03:59

本文主要是介绍小仙女讲JVM(6)—字节码执行引擎,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

JVM中的执行引擎在执行java代码的时候,一般有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。

栈帧
  • 定义:

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它位于虚拟机栈里面。

  • 作用:

每个方法从调用开始到执行完成的过程中,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

  • 特点:

(1)栈帧包括了局部变量表,操作数栈等,到底是需要多大的局部变量表,多深的操作数栈是在编译期确定的。因为一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响。

(2)两个栈帧之间的数据共享。在概念模型中,两个栈帧是完全独立的,但是在虚拟机的实现里会做一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用时,就可以共用一部分数据,无须进行额外的参数复制传递。

(1)局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

//方法参数   
max(int a,int b)

int a;//全局变量
void say(){int b=0;//局部变量}
  • 局部变量和类变量(用static修饰的变量)不同

类变量有两次赋初始值的过程:准备阶段(赋予系统初始值)和初始化阶段(赋予程序员定义的初始值)。所以即使在初始化阶段没有为类变量赋值也没关系,它仍然有一个确定的初始值。
但局部变量不一样,如果定义了,但没有赋初始值,是不能使用的。

(2)操作栈

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈、入栈操作。
在这里插入图片描述
例如,计算:

int a=2+3

操作数栈中最接近栈顶的两个元素是2和3,当执行iadd指令时,会将2和3出栈并相加,然后将相加的结果5入栈。

(3)动态链接

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用分为两部分:

  • 静态解析:在类加载阶段或第一次使用的时候就转化为直接引用。
  • 动态链接:在每一次运行期间转化为直接引用。

(4)返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法:正常退出、异常退出。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。

  • 当方法正常退出时

调用者的PC计数器作为返回地址。栈帧中一般会保存这个计数器值。

  • 当方法异常退出时

返回地址是要通过异常处理器表来确定的。栈帧中一般不会保存这部分信息。

方法调用

方法调用是确定调用哪一个方法。

(1)解析

对“编译器可知,运行期不可变”的方法进行调用称为解析。符合这种要求的方法主要包括

  • 静态方法,用static修饰的方法
  • 私有方法,用private修饰的方法

(2)分派

分派讲解了虚拟机如何确定正确的目标方法。分派分为静态分派和动态分派。讲解静动态分派之前,我们先看个多态的例子。

Human man=new Man();

在这段代码中,Human为静态类型,其在编译期是可知的。Man是实际类型,结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么。

  • 静态分派:

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。它的典型应用是重载。

public class StaticDispatch{  static abstract class Human{  }  static class Man extends Human{}static class Woman extends Human{}public void say(Human hum){  System.out.println("I am human");  }  public void say(Man hum){  System.out.println("I am man");  }  public void say(Woman hum){  System.out.println("I am woman");  }  public static void main(String[] args){  Human man = new Man();  Human woman = new Woman();  StaticDispatch sr = new StaticDispatch();  sr.say(man);  sr.say(woman);  }  
}  

运行结果是:

I am human
I am human

为什么会产生这个结果呢?
因为编译器在重载时,是通过参数的静态类型而不是实际类型作为判断依据的。在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本,所以两个对say()方法的调用实际为sr.say(Human)。

  • 动态分派:

在运行期根据实际类型确定方法执行版本的分派过程。它的典型应用是重写。

public class DynamicDispatch{  static abstract class Human{  protected abstract void say();}  static class Man extends Human{@Overrideprotected abstract void say(){System.out.println("I am man");  }}static class Woman extends Human{@Overrideprotected abstract void say(){System.out.println("I am woman ");  }}public static void main(String[] args){  Human man = new Man();  Human woman = new Woman();  man.say();woman.say();man=new Woman();man.say();}  
}  

运行结果:

I am man
I am woman 
I am woman 

这似乎才是我们平时敲的java代码。对于方法重写,在运行时才确定调用哪个方法。由于Human的实际类型是man,因此调用的是man的name方法。其余的同理。
动态分派的实现依赖于方法区中的虚方法表,它里面存放着各个方法的实际入口地址。如果某个方法在子类中被重写了,那子类方法表中的地址将会替换为指向子类实现版本的入口地址,否则,指向父类的实现入口。

  • 单分派和多分派:

方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,分为单分派和多分派。
在静态分派中,需要调用者的实际类型和方法参数的类型才能确定方法版本,所以其是多分派类型。在动态分派中,已经知道了参数的实际类型,所以此时只需知道方法调用者的实际类型就可以确定出方法版本,所以其是单分派类型。综上,java是一门静态多分派,动态单分派的语言。

字节码解释执行引擎

虚拟机中的字节码解释执行引擎是基于栈的。下面通过一段代码来仔细看一下其解释的执行过程。

public int calc(){  int a = 100;  int b = 200;  int c = 300;  return (a + b) * c;  
}  

第一步:将100入栈。

第二步:将操作栈中的100出栈并存放到局部变量中。后面的200,300同理。

第三步:将局部变量表中的100复制到操作数栈顶。

第四步:将局部变量表中的200复制到操作数栈顶。

第五步:将100和200出栈,做整型加法,最后将结果300重新入栈。

第六步:将第三个数300从局部变量表复制到栈顶。接下来就是将两个300出栈,进行整型乘法,将最后的结果90000入栈。

第七步:方法结束,将操作数栈顶的整型值返回给此方法的调用者。

这篇关于小仙女讲JVM(6)—字节码执行引擎的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/796562

相关文章

Java中Map的五种遍历方式实现与对比

《Java中Map的五种遍历方式实现与对比》其实Map遍历藏着多种玩法,有的优雅简洁,有的性能拉满,今天咱们盘一盘这些进阶偏基础的遍历方式,告别重复又臃肿的代码,感兴趣的小伙伴可以了解下... 目录一、先搞懂:Map遍历的核心目标二、几种遍历方式的对比1. 传统EntrySet遍历(最通用)2. Lambd

Spring Boot 中 RestTemplate 的核心用法指南

《SpringBoot中RestTemplate的核心用法指南》本文详细介绍了RestTemplate的使用,包括基础用法、进阶配置技巧、实战案例以及最佳实践建议,通过一个腾讯地图路线规划的案... 目录一、环境准备二、基础用法全解析1. GET 请求的三种姿势2. POST 请求深度实践三、进阶配置技巧1

springboot+redis实现订单过期(超时取消)功能的方法详解

《springboot+redis实现订单过期(超时取消)功能的方法详解》在SpringBoot中使用Redis实现订单过期(超时取消)功能,有多种成熟方案,本文为大家整理了几个详细方法,文中的示例代... 目录一、Redis键过期回调方案(推荐)1. 配置Redis监听器2. 监听键过期事件3. Redi

Spring Boot 处理带文件表单的方式汇总

《SpringBoot处理带文件表单的方式汇总》本文详细介绍了六种处理文件上传的方式,包括@RequestParam、@RequestPart、@ModelAttribute、@ModelAttr... 目录方式 1:@RequestParam接收文件后端代码前端代码特点方式 2:@RequestPart接

SpringBoot整合Zuul全过程

《SpringBoot整合Zuul全过程》Zuul网关是微服务架构中的重要组件,具备统一入口、鉴权校验、动态路由等功能,它通过配置文件进行灵活的路由和过滤器设置,支持Hystrix进行容错处理,还提供... 目录Zuul网关的作用Zuul网关的应用1、网关访问方式2、网关依赖注入3、网关启动器4、网关全局变

SpringBoot全局异常拦截与自定义错误页面实现过程解读

《SpringBoot全局异常拦截与自定义错误页面实现过程解读》本文介绍了SpringBoot中全局异常拦截与自定义错误页面的实现方法,包括异常的分类、SpringBoot默认异常处理机制、全局异常拦... 目录一、引言二、Spring Boot异常处理基础2.1 异常的分类2.2 Spring Boot默

基于SpringBoot实现分布式锁的三种方法

《基于SpringBoot实现分布式锁的三种方法》这篇文章主要为大家详细介绍了基于SpringBoot实现分布式锁的三种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、基于Redis原生命令实现分布式锁1. 基础版Redis分布式锁2. 可重入锁实现二、使用Redisso

SpringBoot的全局异常拦截实践过程

《SpringBoot的全局异常拦截实践过程》SpringBoot中使用@ControllerAdvice和@ExceptionHandler实现全局异常拦截,@RestControllerAdvic... 目录@RestControllerAdvice@ResponseStatus(...)@Except

Springboot配置文件相关语法及读取方式详解

《Springboot配置文件相关语法及读取方式详解》本文主要介绍了SpringBoot中的两种配置文件形式,即.properties文件和.yml/.yaml文件,详细讲解了这两种文件的语法和读取方... 目录配置文件的形式语法1、key-value形式2、数组形式读取方式1、通过@value注解2、通过

Java 接口定义变量的示例代码

《Java接口定义变量的示例代码》文章介绍了Java接口中的变量和方法,接口中的变量必须是publicstaticfinal的,用于定义常量,而方法默认是publicabstract的,必须由实现类... 在 Java 中,接口是一种抽象类型,用于定义类必须实现的方法。接口可以包含常量和方法,但不能包含实例