执行引擎

关于执行引擎相关的部分, 在之前的博文里

Java内存区域中已经有所提及.

回顾一下:

也只有几个概念, JVM方法调用和执行的基础数据结构是 栈帧, 是内存区域中 虚拟机栈中的栈元素, 每一个方法的执行就对应着一个栈帧在虚拟机栈中出栈入栈的过程.

栈帧:则是包含有局部变量表, 操作数栈, 动态连接, 方法返回地址, 附加信息.

  1. 局部变量表:

    存储单位是 slot, 一个slot占据32位, 对于64位的数据类型, 则是分配连续两个slot空间. 而对于一个非静态方法而言, 有一个隐藏参数, 为 this, 而在局部变量表中的变量存储顺序则是

    this -> 方法参数 -> 方法体内的变量(slot可以重用, 超出作用域即可复用.) 方法在编译完成后, 其所需的空间已经确定.

    (这里也是需要注意的一个地方, 变量的作用域常常会覆盖整个方法, 即使变量已经不再使用, 但只要还在作用域内, 其slot空间就无法给其他变量使用, 因此, 最好是在需要使用到变量时, 定义在合理的作用域范围内.)

  2. 操作数栈:

    在操作数栈中需要注意,其数据类型必须与字节码指令的序列严格匹配.

  3. 动态连接: 稍后详解

  4. 方法返回地址:

    方法有两种退出方式, 正常退出, 异常退出, 当正常退出后, 会恢复上层方法的局部变量表, 操作数栈, 并把方法返回结果压入调用者的操作数栈.

方法调用

方法调用阶段的唯一目的是, 确定调用方法的版本究竟是哪一个.

在Java虚拟机中提供了5条方法调用的相关指令:

invokestatic: 调用静态方法

invokespecial: 调用实例构造器方法, 私有方法, 父类方法

invokevirtual: 调用所有的虚方法

invokeinterface: 调用所有的接口方法

invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法, 然后再执行该方法.

虚方法是非虚方法的补集, 什么是非虚方法呢? 能够在编译器就确定将要调用的究竟是哪个方法, 进而将该方法的符号引用 转换为 相应的直接引用的 方法就被称作非虚方法.

我们知道在类加载时, 在相应的类信息中, 存有对应方法的相关信息, 常量池中存有相关直接引用. 在类加载的解析阶段, 即会将这部分的符号引用转换为直接引用.

那么什么方法才满足这种条件呢?

能够被invokespecial 和 invokestatic指令调用的方法, 都是可以在编译器确定的方法, 即静态方法, 私有方法, 父类方法(super.), 实例构造器.

在final方法是个特殊点, 虽然final方法的执行为 invokevirtual, 但它依然属于非虚方法, 不难理解, final方法不能够被重写.

方法分派(dispatch)

  1. 静态分派

    对于代码

     Human man = new Man();

    其中Human被称为变量的静态类型, 也叫外观类型, 而 Man则是变量的实际类型. 而一个变量的静态类型, 在声明时即已经确定, 仅仅在使用时才能够临时转换静态类型, 但变量本身的静态类型并不会改变, 实际类型的变化只有在运行期才能确定.

     //实际类型变化
    Human man = new Man();
    man = new Woman();
    //静态类型的变化
    method((Man) man);
    method((Woman) man);

    而当我们在重载方法时, 向方法中传入的参数类型, 即是静态类型.因此 重载是一种 可以在编译期就被确定执行方法版本 的行为.

  2. 动态分派

    动态分派 与 重写息息相关.

     static class Human{
    void sayHello() {
    System.out.println("human say hello");
    }
    } static class Man extends Human{
    @Override
    void sayHello() {
    System.out.println("man say hello");
    }
    } void sayHello(Human man) {
    man.sayHello();
    } public static void main(String[] args) {
    Human man = new Man();
    Human human = new Human();
    new Main().sayHello(man);
    new Main().sayHello(human);
    } //out:
    man say hello
    human say hello

    结果不必多做解释, 而现在的问题在于, 虚拟机如何知道, 究竟调用的是哪个方法?

      0: new           #3                  // class Main$Man
    3: dup
    4: invokespecial #4 // Method Main$Man."<init>":()V
    7: astore_1
    8: new #5 // class Main$Human
    11: dup
    12: invokespecial #6 // Method Main$Human."<init>":()V
    15: astore_2
    16: new #7 // class Main
    19: dup
    20: invokespecial #8 // Method "<init>":()V
    23: aload_1
    24: invokevirtual #9 // Method sayHello:(LMain$Human;)V
    27: new #7 // class Main
    30: dup
    31: invokespecial #8 // Method "<init>":()V
    34: aload_2
    35: invokevirtual #9 // Method sayHello:(LMain$Human;)V
    38: return

    其中主要关注几个方法的执行点, invokespecial不用多说, 之前提到过, 是执行 构造器方法时 的指令

    而 invokevirtual 则正是执行 main.sayHello(), 方法的指令, 指令的运行时解析过程大致如下:

    1. 找到操作数栈顶的第一个元素的所指对象的实际类型, 记做C

    2. 如果在C中找到与描述符 和 简单名称都相符的方法, 进行访问校验, 如果可以则返回方法的直接引用, 否则抛出 IllegalAccessError异常

    3. 否则按照继承关系 从下向上对C的各个父类进行第二步的搜索验证过程.

    4. 如果始终找不到, 抛出异常.

    而其中的关键点就在于, 取到的是 对象的实际类型.

动态类型语言

这也是要提到的关于 invokedynamic指令的主要目的。

动态类型语言的概念是: 意思就是类型的检查是在运行时做的而非编译期。

而Java本身则是静态类型语言, 这一点又在哪里能够体现呢?

obj.println("language");

如果处在java环境中,且obj的静态语言类型是 java.io.PrintStream, 那么obj本身的实际类型也必须是PrintStream的子类才行, 哪怕本身存在 println方法也不可以, 但同样的问题放在 javascript中就不同了, 只要实际类型中存在println方法, 执行就不会有任何问题.

这点就是因为, java在编译时已经将其完整的符号引用生成出来, 如果注意到的话, 会发现无论是动态分派还是静态分派, 在编译的指令中都是已经精确到相应类的某一个方法中了, 如此, 自然只能够在有限的范围内略做调整, 如果超出了当前类的范围, 就无法调用了.

jvm虚拟机并不仅仅是java语言的虚拟机, 那么如何为动态类型语言提供支持就是一个问题了, 并且在目前java8中的lamda表达式中也应用的是 invokedynamic指令.

MethodHandle

而与之相关的jar包则是 java.lang.invoke, 相关的类则是 MethodHandle.

在这里我也并不想再谈 MethodHandle的使用方法, 网上资料实在不少.

需要提到的是, 它的功能和java的反射略有相似, 通过方法名, class, 就可以调用相应的方法. 但它比起反射要轻量级; 且Reflection是在模拟Java代码的调用, MethodHandle是在模仿字节码层面的调用.

这个方法不失为是在动态调用中除了反射之外的另一种选择.

基于栈解释器的执行过程

其实本文更像是在 前一篇博客中 java内存区域中的虚拟机栈的一种补充说明.

而真实的执行流程, 我想通过下文的代码来看:

public int add() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
} -- javap -verbose Main public int add();
// 返回类型为 int
descriptor: ()I
flags: ACC_PUBLIC
Code:
//需要深度为2的操作数栈, 4个slot的局部变量空间, 有一个参数为 this
stack=2, locals=4, args_size=1
//将100推入操作数栈顶, 栈:100
0: bipush 100
//将栈顶的数据出栈并存储到局部变量表的第一个slot中(从0开始)
//此时:栈: - 局部变量表: slot1 100
2: istore_1
//与上面类似,重复过程
3: sipush 200
6: istore_2
7: sipush 300
//此时:栈: - 局部变量表: slot1 100 slot2 200 slot3 300
10: istore_3
//将局部变量表 slot1的值复制到 栈顶
11: iload_1
//将局部变量表 slot2的值复制到 栈顶 此时:栈: 200 100
12: iload_2
//栈顶两个元素出栈, 并相加, 结果重新入栈. 此时: 栈: 300
13: iadd
//将局部变量表 slot3的值复制到 栈顶 此时:栈: 300 300
14: iload_3
//将栈顶元素相乘, 结果重新入栈
15: imul
//将栈顶的结果返回给方法调用者. 方法执行结束
16: ireturn
LineNumberTable:
line 85: 0
line 86: 3
line 87: 7
line 88: 11
//局部变量表, 无需多言.
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this LMain;
3 14 1 a I
7 10 2 b I
11 6 3 c I

基于栈的执行引擎正是通过这样出栈入栈的方式完成指令, 而基于寄存器的则不然, 是将操作数存入寄存器, 同时将输入值也就是指令参数 与 某寄存器的存储值相加. 区别就在于存储位置, 以及参数问题, 基于栈的大部分指令都是无参数指令, 指令很明确的规定了 要用哪几个栈元素, 栈元素的类型是什么.

我们平常所使用的电脑, 其 X86指令集, 正是基于寄存器的指令集.

优缺点则是: 基于栈, 可移植性较强, 但速度比较慢, 慢的原因一是需要许多冗余操作, 代码. 二是基于栈是基于内存的操作方式, 而内存的速度比起寄存器更是要慢上许多.

至于寄存器为什么比内存更快?

为什么寄存器比内存快?

总结

本文大致介绍这样几点:

java多态在 jvm层次的实现.

为什么说jvm执行引擎是基于栈的执行引擎, 以及究竟是怎样一个流程.

Java虚拟机执行引擎的更多相关文章

  1. jvm虚拟机---执行引擎子系统

    Java虚拟机只与Class文件相关联,它规定了Class文件应该具有的格式,而不论该文件是由什么语言编写并编译而来.所以,任何语言只要能够最终编译成符合Java虚拟机要求的Class文件,就可以运行 ...

  2. 重读《深入理解Java虚拟机》五、虚拟机如何执行字节码?程序方法如何被执行?虚拟机执行引擎的工作机制

    Class文件二进制字符流通过类加载器和虚拟机加载到内存(方法区)完成在内存上的布局和初始化后,虚拟机字节码执行引擎就可以执行相关代码实现程序所定义的功能.虚拟机执行引擎执行的对象是方法(均特指非本地 ...

  3. 重读《深入理解Java虚拟机》三、Java虚拟机执行的数据入口(类文件结构)

    1.Java如何实现平台无关系 Java要实现平台无关系就需要在Java代码和本地机器之间引入一个中间层,实现Java代码和本地机器的解耦,而这个中间层就是字节码.字节码独立于本地机器,以实现代码的可 ...

  4. (转)《深入理解java虚拟机》学习笔记9——并发编程(一)

    随着多核CPU的高速发展,为了充分利用硬件的计算资源,操作系统的并发多任务功能正变得越来越重要,但是CPU在进行计算时,还需要从内存读取输出,并将计算结果存放到内存中,然而由于CPU的运算速度比内存高 ...

  5. 详细解析Java虚拟机的栈帧结构

    欢迎关注微信公众号:万猫学社,每周一分享Java技术干货. 什么是栈帧? 正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器.虚拟机栈.本地方法栈.堆和方法区.(什么?你还不知道,赶紧去看看 ...

  6. 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的

    概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...

  7. 深入理解Java虚拟机(字节码执行引擎)

    深入理解Java虚拟机(字节码执行引擎) 本文首发于微信公众号:BaronTalk 执行引擎是 Java 虚拟机最核心的组成部分之一.「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力, ...

  8. Java虚拟机-字节码执行引擎

    概述 Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,成为各种虚拟机执行引擎的统一外观(Facade).不同的虚拟机引擎会包含两种执行模式,解释执行和编译执行. 运行时帧栈结构 栈帧(Sta ...

  9. JAVA虚拟机:虚拟机字节码执行引擎

    “虚拟机”是一个相对“物理机”的概念,这两种机器都有代码执行能力. 物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的. 虚拟机的执行引擎由自己实现,自行制定指令集与执行引擎的结构体系 ...

随机推荐

  1. Linux的简单介绍和开发基本运维时候用到的命令

    先简单介绍下Linux文件夹目录 1./ linux下的根目录 实际上等同于window的我的电脑点进去 2./etc /usr 一个是系统配置文件存放的地方,一个是系统资源(应用程序)放的地方这俩文 ...

  2. 【Win】使用ScreenToGif制作gif动态图片

    ScreenToGif 经常要写各类教程,有时候需要制作一些演示动画,GIF动画图片是个不错的选择,ScreenToGif是一款GIF录屏软件,下载地址可自行百度. 运行环境 操作系统:windows ...

  3. 【JAVA】通过URLConnection/HttpURLConnection发送HTTP请求的方法(一)

    Java原生的API可用于发送HTTP请求 即java.net.URL.java.net.URLConnection,JDK自带的类: 1.通过统一资源定位器(java.net.URL)获取连接器(j ...

  4. 2018.09.21 codeforces1051D. Bicolorings(线性dp)

    传送门 sb线性DP. f[i][j][0/1/2/3]f[i][j][0/1/2/3]f[i][j][0/1/2/3]表示前i列j个连通块且第i列状态为00/01/10/11时的方案总数. 这个显然 ...

  5. 2018.09.06 烽火传递(单调队列优化dp)

    描述 烽火台是重要的军事防御设施,一般建在交通要道或险要处.一旦有军情发生,则白天用浓烟,晚上有火光传递军情. 在某两个城市之间有 n 座烽火台,每个烽火台发出信号都有一定的代价.为了使情报准确传递, ...

  6. xampp环境 安装 用法 composer

    准备工作 1.打开PHP配置文件E:\xampp\php\php.ini确认以下模块已开启(移除前面的分号). extension=php_openssl.dll, extension=php_cur ...

  7. Django入门与实践-第22章:基于类的视图

    http://127.0.0.1:8000/boards/1/topics/2/posts/2/edit/ http://127.0.0.1:8000/ #boards/views.py from d ...

  8. if_elseif

    用MATLAB写了个这样的程序 if ((0 < pwr <=2) ) wf_temp1 = round(temp_wf0/2^7); elseif( (2 < pwr<= 4 ...

  9. wadl 的自动生成(cxf版本3.1.1)

    官方文档 http://cxf.apache.org/docs/jaxrs-services-description.html 举例: package cn.zno; import javax.ws. ...

  10. MFC 怎样获得某个窗口的句柄?

    GetSafeHandle();this-> hWnd;GetDlgItem(hwnd,ID);//获取窗口ID所对应的HWND的子窗口句柄 在主窗口中,如果要用到父窗口的句柄,可以用 HWND ...