精华内容
下载资源
问答
  • 虚拟机字节码执行引擎 文章目录虚拟机字节码执行引擎运行时栈帧结构局部变量表操作数栈动态链接方法返回地址方法调用解析分派静态分派动态分派单分派与多分派JVM动态分派的实现基于栈的字节码解释执行引擎参考文献 ...

    虚拟机字节码执行引擎

    运行时栈帧结构

    栈帧是虚拟机进行方法调用和执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧中存储了局部变量表、操作数栈、动态链接、方法返回地址等信息,每个方法的调用直到完成都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    局部变量表

    局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。在将Java文件编译为Class文件时,就在方法的Code属性max_locals数据项中确定了该方法所要分配的局部变量表的最大空间。局部变量表的容量以变量槽(slot)为最小单位。

    局部变量表建立在线程的堆栈上,是线程私有的,不会出现线程安全问题。虚拟机通过索引定位方式使用局部变量表,索引范围从0到局部变量表的最大的slot数量。对于非static的实例方法,局部变量表中第0位的slot默认传递的是方法所属对象的引用(即this指针)。slot是可以复用的,若当前字节码的PC值已经超出某变量的作用域,那么该变量对应的slot就可以被其他变量复用,以此来节省栈空间。

    操作数栈

    操作数栈又称为操作栈,是一个后入先出栈,其最大深度在编译的时候写入到Code属性的max_stacks数据项中,在方法执行时操作栈的深度不能超过max_stacks属性设定的最大值。方法开始时为空,方法执行过程中会有各种字节码指令往操作数栈中写入或弹出,也就是对应着入栈和出栈操作。

    多个操作数栈是互相独立的,但是大多数的虚拟机实现会做一些优化,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠起来,通过这些共享区域,使得方法调用时可以共用一部分数据,避免额外的参数复制传递。

    动态链接

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,用于支持方法调用过程中的动态链接。一部分符号引用在类加载或第一次使用时转化为直接引用,叫做静态解析;另外一部分符号引用在每次运行期间转化为直接引用,叫做动态链接。

    方法返回地址

    方法执行后有两种方式退出该方法:

    1. 执行引擎遇到一个方法返回的字节码指令,将返回值传给调用者,该方式称为正常完成出口
    2. 方法执行过程中遇到了异常而在方法体中未处理,导致方法退出,这种方式称为异常完成出口,它不会产生任何的返回值给它的调用者

    方法退出后要回到方法被调用的位置,程序才能继续执行,方法返回时要在栈帧中保存一些信息,用来帮助恢复到它的上层方法的执行状态。方法退出等同于把当前栈帧出栈:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC值以指向方法调用者指令的后一条指令。

    方法调用

    方法调用不等同于方法执行。方法调用是为了确定被调用方法的版本,暂不涉及方法内的具体运行。所有的方法调用在class文件中存储的都是符号引用,而不是方法在实际运行时内部内存中的入口地址(即直接引用),在类加载甚至是运行期间才能确定目标方法的直接引用。

    解析

    调用目标在程序写好、编译器编译时就确定下来的这类方法的调用称为解析。主要有静态方法和私有方法两大类,它们都是在类加载的阶段解析。

    在JVM里提供了5条方法调用的字节码指令:

    1. invokestatic调用静态方法
    2. invokespecial调用实例构造器<init>、私有方法或父类方法
    3. invokevirtual调用虚方法
    4. invokeinterface调用接口方法,在运行时确定实现该接口方法的对象
    5. invokedynamic先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

    前四条指令的分配逻辑是固定在Java虚拟机内部的,而第五条指令的分派逻辑是根据用户设定的引导方法决定的。解析调用是一个静态过程,在编译期就能完全确定下来,在类加载时就会将符号引用转化为直接引用,不会延迟到运行期才去执行。

    分派

    静态分派

    所有依赖静态类型来定位方法执行版本的分派动作叫做静态分派。静态分派发生在编译期,不是由虚拟机执行的,它是重载的本质。

    动态分派

    在运行期确定接收者的实际类型以确定方法执行版本的分派动作称为动态分派。动态分派发生在方法实际运行时期,它是重写的本质。

    单分派与多分派

    方法的接收者和方法参数统称为宗量。根据分派基于多少种宗量,可以划分为单分派和多分派。Java语言的静态分派属于多分派;动态分派属于单分派。

    JVM动态分派的实现

    常用的稳定优化手段:为类在方法区建立一个虚方法表(接口方法表),用虚方法表索引来代替元数据查找以提高性能,虚方法表中存储了各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表中的方法入口地址和父类相同方法的入口地址是一样的,都指向父类的方法入口地址;如果子类重写了父类方法,那么子类虚方法表中的方法入口地址则指向子类实现的方法入口地址。

    除了使用虚方法表这一稳定的优化手段外,如果条件允许,还会使用内联缓存和基于类型继承关系分析这两种非稳定的激进优化方法,来获得比较高的性能。

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

    1. 解释执行

      程序源码 --> 词法分析 --> 单词流 --> 语法分析 --> 抽象语法树 --> 指令流 --> 解释器 -->解释执行

    2. 编译执行

      程序源码 --> 词法分析 --> 单词流 --> 语法分析 --> 抽象语法树 --> 优化器 --> 中间代码 --> 生成器 -->目标代码

    3. 基于栈的指令集与基于寄存器的指令集
      Java编译器输出的指令流基本上是一种基于栈的指令集架构,指令大部分为零地址指令,依赖操作数栈工作。具有可移植性好、代码紧凑、编译器实现简单的优点,但执行速度较慢。
      基于寄存器的指令集依赖寄存器工作,寄存器由硬件直接提供,所以会受到硬件条件的约束,但是这种架构与基于栈的指令集架构相比,完成相同功能所需要的指令数量一般会比较少,速度会更快。

    4. 基于栈的解释执行

      中间变量都以操作数栈的出栈和入栈做为信息交换途径。

    参考文献

    深入理解Java虚拟机

    展开全文
  • 虚拟机字节码执行引擎 执行引擎是JAVA虚拟机最核心的组成部分之一。‘虚拟机’不同于‘物理机’概念,它是区别‘物理机’的执行引擎直接建立在处理器、硬件、指令集和操作系统上的,‘虚拟机’的执行引擎是有自己...

    虚拟机字节码执行引擎

    执行引擎是JAVA虚拟机最核心的组成部分之一。‘虚拟机’不同于‘物理机’概念,它是区别‘物理机’的执行引擎直接建立在处理器、硬件、指令集和操作系统上的,‘虚拟机’的执行引擎是有自己实现的,自行指定指令集与执行引擎的结构体系。
    在Java的虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个模型成为各种虚拟机的执行引擎的统一外观。
    执行引擎在执行Java代码的时候不论是解释执行或编译执行或两者兼备甚至 其他级别的编译器执行引擎。从外观上看起来,他们执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
    从概念模型的角度分析虚拟机的方法调用和字节码执行。

    运行时栈帧结构

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧中存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从开始调用到执行完成,都对应这一个栈帧在虚拟机里面从入栈到出栈的过程。
    每一个帧栈需要分配的内存,在编译程序代码的时候,已经确定并写入到方法表的Code属性之中了。因此帧栈的内存不会受到程序运行期变量数据的影响,仅仅取决于虚拟机的具体实现。
    一个线程的方法执行调用链可能会很长,很多方法同时处于执行状态。对于执行引擎来说,在活动线程中,只有栈顶的帧栈是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)栈帧的概念结构

    • 局部变量表
      局部变量表是一组变量值储存空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编程成Class文件时,就在方法的Code属性的max_locals数据项中确定的该方法的局部变量表的最大容量。
      局部变量表的容量以变量槽(Variable Slot)为最小单位。
      每个Slot都应该能存放一个Boolean、byte、char、short、int、float、reference或returnAddress,这种描述明确指出‘每个Slot占用32位长度的内存空间’,虽然允许Slot随着处理器、操作系统和虚拟机的不同而发生变化。但是不论如何虚拟机都要使用补齐和补白的手段让Slot在外观上看起来与32位虚拟机一致。
      reference是对象的引用。returnAddress是字节码指令jsr、jsr_w和ret服务的。
      对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。Java语言中明确规定的64位的数据类型只用long和double两种。类似与‘long和double的非原子性协定’在私有的线程上都不会引起数据安全问题。
      虚拟机通过索引定位的方式使用局部变量表,索引范围是从0到局部变量表的最大Slot数量。32位的数据类型索引n就代表使用的第n个Slot,64位的数据类型就会使用第n个和第n+1个的两个Slot。
      局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其他的变量使用。
      在这里插入图片描述在这里插入图片描述
      我们向内存填充64MB数据,然后通知虚拟机进行垃圾回收。发现并没有回收placeholder。placeholder还在作用域中所有无法回收。
      在这里插入图片描述
      在这里插入图片描述
      修改后,将placeholder放入花括号中,我们发现仍然没有被回收
      在这里插入图片描述
      在这里插入图片描述
      在修改 ,发现这次就修改成功了。
      placeholder能否被回收的根本原因就是:局部变量表中的Slot是否还存有关于placeholder数组对象的引用。第一次修改虽然代码离开了 placeholder 的作用域但是placeholder的Slot仍然被占用着或没有被复用所以GC Roots的一部分局部变量表对他任然有着联系。所以无法被回收。
      第二次修改我们手动设置null值(用来代替int a = 0;那变量对应的局部变量表Slot清空)。
      这种方式及其特殊,不经常使用。以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。

    • 操作数栈
      操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out)栈。操作数栈的最大深度也是在编译的时候写入到Code属性的max_stacks数据项之中。操作数栈可以是任意的Java数据类型包括long和double。32位所占栈容量为1,64位为2。
      当一个方法开始执行的时候,这个时候这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容。也就是入栈出栈操作。Java方法运行的时候都是通过操作数栈来进行的,或者在调用其他的方法的时候通过操作数栈来进行传递参数。
      在概念模型中。两个栈帧是完全独立的,但是大多数虚拟机都会对此进行优化,令两个栈帧有部分重叠,这样就可以共用这部分数据,无需额外的参数复制传递。两个栈帧之间的数据共享

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

    • 方法返回地址
      当一个方法被执行后,有两种方式退出这个方法。第一种就是执行引擎遇到任何一个任意方法的返回字节码指令,这种退出的方式被称为正常完成出口(Normal Method Invocation Completion)。
      另一种就是执行方法遇到了异常,无论是Java虚拟机内部产生的异常,还是代码编写异常,都会导致方法退出,这种退出方式被称为异常完成出口(Abrupt Method Invocation Completion)。异常完成出口是不会给上层调用者任何的返回值的。

      方法的退出实际上等同于当前帧栈出栈,不论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行。帧栈会保存方法返回时的一些信息,用来帮助恢复它的上层方法的执行状态。正常方式退出调用者的PC技术器的值就可以作为返回地址。帧栈中很可能就保存这个值。异常退出,返回地址是要通过 异常处理器来确定,而帧栈一般不会报错这个信息。

    • 附加信息
      虚拟机规范允许具体虚拟机实现增加一些规范中没有描述的信息到帧栈中,在实际开发中,一般是把动态连接、方法返回地址与其他附加信息全部归为一类,称为帧栈信息。

    方法调用

    方法调用并不等同于方法执行。方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂不涉及运行。

    • 解析
      用目标在程序代码写好、编译器进行编译是必须确定下来。这类方法的调用称为解析(Resolution),在Java语言中,符合‘编译期可知,运行期不可变’这个要求的方法主要有静态方法和私有方法两大类,前者与类型直接关联。后者在外部不可被访问,这两种方法都不可能通过集成或别的方式重写出其他的版本。因此它们都适合在类加载阶段进行解析。
      与之相对应,Java虚拟机提供了四条方法调用字节码指令:
      1)invokestaic:调用静态方法
      2)invokespecial:调用实例构造器方法、私有方法和父类方法。
      3)invokevirtual:调用所有的虚方法
      4)invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象。

      只要能被invokestaic和invokespecial指令调用的方法,都可以在解析阶段确定版本。符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类。它们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为非虚方法。其他的就是虚方法(除去final外),final虽然是使用invokevirtual调用,但是它无法被覆盖,没有其他的版本,所以Java规定final方法是非虚方法
      方法静态解析演示

    • 分派(Dispatch)
      解析调用是一个静态的过程,在编译期就完全确定,不会再运行期完成。而分派调用则可能是静态的也可能是动态的。根据分派依据的宗量数可分为单分派和多分派,再细分就是静态单分派、静态多分派、动态单分派和动态多分派。
      一个简单的面试题
      在这里插入图片描述
      上面是主要是方法的重载,我们把把‘Human’称为静态类型(Static Type)或者外观类型(Apparent Type),后面的‘Man’‘Woman’称为实际类型(Actual Type),静态类型是在编译期就可以知道,实际类型是在运行时才会确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。代码中刻意的使用类静态类型一致,实际类型不同的变量。编译器在重载是通过静态类型作为依据的。所以选中了sayHello(Human)作为调用目标。并把这个方法符号写到invokevirtual指令参数中。
      所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。编译器虽然能确定出方法的重载版本,但多数情况下这个重载版本不唯一,往往只能确定一个‘更合适的版本’,一下演示了何为‘更合适的版本’
      在这里插入图片描述
      ‘a’是个 char类型的 静态类型,所以直接去找到版本sayHello(char arg),将这个注释后,输入结果为int 2 ‘a’除了可以代表一个字符串外,还可以用数字97 代替,自动转型还能发生多次 char->int->long->float->double的顺序进行匹配不会匹配到byte和short的类型是不安全的。在注释 之后就是character 4 ,发生自动装箱。将‘a’包装为它的包装类Character,在注释是Serializable,是因为Character实现了Serializable接口。
      在这里插入图片描述
      在注释就是能是Object,这个超类了,在注释就只剩可变参数的了。这是‘a’是作为一个数组元素。
      静态方法会在类加载期就进行解析。而静态方法显然也是可以有重载版本的,选择重载版本是通过静态分派完成的。
      动态分派和重写(Override)有密切的关联。
      ab
      这里是根据实际类型进行分派的
      由于invokevirtual指令执行的第一步就是在运行期确定接受者的实际类型。所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到不同直接引用上,这个过程就是Java的重写本质,我们把在运行期根据实际类型确定方法版本的分派称为动态分派。

    • 单分派和多分派

    方法的接收者与方法的参数统称为方法的宗量根据分派基于多少宗量,可以分为单分派和多分派。单分派是根据一个宗量对目标进行选择。多分派就是多于一个宗量。
    在这里插入图片描述
    对于静态分派需要判断静态类型和方法参数,所以一般静态分派属于多分派类型。
    而动态分派在编译时已经确定方法参数,所以只用区别实际类型,只有一个宗量,所以一般动态分派属于单分派。
    虚拟机动态分派的实现
    基于性能优化会在方法区建立一个虚方法表(Virtual Method Table),对应的invokeinterface执行的时候也会用到接口的方法表(Interface Method Table)
    在这里插入图片描述
    使用虚方法表索引来代替元数据查找以提高性能,对于方法签名一致的方法,父类和子类在虚索引表中都具有一样的索引,当类型转变时,只用转换方法表就好了。

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

    许多Java虚拟机执行引擎在执行代码时,都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)。

    1. 解释执行
      在这里插入图片描述
      Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析、到抽象语法树,在遍历语法树生成线性的字节码指令流的过程。这部分是在Java虚拟机之外执行的,而解释器是在虚拟机内部,所以Java程序的编译就是半独立实现的。
    2. 基于栈的指令集与基于寄存器的指令集
      基于栈的指令集是依赖操作数栈进行工作。
      基于寄存器的指令集我们主流PC中直接支持的指令集架构。
      简单的例子:
      基于栈: 计算1+1
      在这里插入图片描述
      连续把两个常量压入栈后,iadd指令把栈顶的两个值出栈 相加,再放回栈顶istore_0在放到局部变量表的第0个Slot中
      基于寄存器:计算1+1
      在这里插入图片描述
      mov指令把EAX寄存器值设为1,add 在加1,结果寄存在EAX中。
      基于栈的 主要优点是可移植性,但是执行速度相对来说慢一点。
      寄存器是由硬件直接提供,程序直接依赖硬件寄存器则不可避免地要受到硬件的约束。
    展开全文
  • 虚拟机字节码执行引擎虚拟机字节码执行引擎1 概述2 运行时栈帧结构1)局部变量表2) 操作数栈3)动态连接4)方法返回地址5)附加信息3 方法调用1)解析2)分派3)单分派与多分派4 基于栈的字节码解释执行引擎 虚拟机...

    虚拟机字节码执行引擎

    1 概述

    在不同的虚拟机实现里面,执行引擎在执行java代码的时候可能有解释执行和编译执行两种选择,也可能两者兼备,甚至还可能包含几个不同级别的编译器执行引擎,但是从外观上看起来,所有的java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行

    2 运行时栈帧结构

    在这里插入图片描述
    在这里插入图片描述

    1)局部变量表

    局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在java程序被编译为class文件时,就在方法的code属性的max_locals数据项中确定了方法所需要分配的最大局部变量表的容量。

    局部变量表的容量以变量槽(Variable Slot)为最小单位。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    2) 操作数栈

    在这里插入图片描述
    注:

    在这里插入图片描述

    3)动态连接

    在这里插入图片描述

    4)方法返回地址

    在这里插入图片描述

    5)附加信息

    3 方法调用

    class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。这个特性给java带来了更强大的动态扩展能力,但也使得java方法的调用过程变得相对复杂起来,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

    1)解析

    在类加载阶段进行解析的情况:调用目标在程序代码写好、编译器进行编译时就必须确定下来。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    2)分派

    分派调用过程将会揭示多态性特征的一些最基本的体现(如重载,重写等等)。

    1. 静态分派
      例子如下:
      在这里插入图片描述
      虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。

    注:重载会查找更合适的版本;

    1. 动态分派
      它和多态性的另外一个重要体现—重写(override)有着很密切的关联。
      找到主动发起调用的类是子类还是父类。
      在这里插入图片描述
      因为调用目标(个人认为就是主动发起调用的类)不确定,从子类一层层向上找方法。

    3)单分派与多分派

    方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个的宗量对目标方法进行选择

    在这里插入图片描述
    个人理解:

    静态分派选好参数类型合适的方法,选好主动发起调用的类为基类;
    动态分派:选好主动发起调用的类是基类还是子类;

    注:虚拟机动态分派的实现:
    在这里插入图片描述

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

    和汇编的理解方式类似,不再赘述。

    展开全文
  • 《深入理解java虚拟机 笔记》第八章 虚拟机字节码执行引擎 第八章 虚拟机字节码执行引擎   

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

      字节码执行引擎是java虚拟机的重要组成部分。执行引擎作用是接收字节码,解析字节码,执行并输出执行结果。不同的虚拟机中,执行引擎在执行java代码时可能会有解释执行(通过解释器实时执行)和编译执行(通过即时编译器产生本地代码执行)两种之一,也可能两者都兼备
      栈帧是支持虚拟机进行方法调用和方法执行的数据结构。它存储在运行时数据区的虚拟机栈中。每一个方法的从开始到完成的过程,都对应了一个栈帧的入栈和出栈的过程。一个栈帧包含了:局部变量表,操作数栈,动态连接,方法返回地址。局部变量表和操作数栈在编译的时候,已经可以完全确定,并且写入到了Class文件的方法表的Code属性之中。因此一个栈帧需要多大的内存,不会受到程序运行期的变量数据影响。
          在这里插入图片描述

    局部变量表中Slot只有被复用,无效局部变量才能垃圾收集成功

      局部变量表用于存放方法参数和方法内部定义的局部变量,在java程序编译为class文件就在方法的Code属性max_locals中确定了该方法需要分配的局部变量表的最大容量。java虚拟机中还有reference和returnAddress数据类型,reference表示一个对象实例的引用,虚拟机可以通过reference直接或间接查找对象在java堆中存放的起始地址索引与在方法区中对象所属的数据类型
      为了尽可能节省栈帧空间,局部变量表中的slot是可以服用的,即方法体中定义的变量作用域不一定覆盖整个方法体,如果字节码PC计数器值超过了某个变量作用域(当执行System.gc()我们想当然以为虚拟机会回收这个局部变量的内存),然而作为GC Roots一部分的局部变量表仍然保持对它的关联,所以无法回收,只有将不再使用的对象置为null,或者其他变量占用了它的slot才能回收。案例分析:运行时再虚拟机参数中加入-verbose:gc

    public class TestLocalVariableTable {
    	public static void main(String[] args) {
    		byte[] placeholder = new byte[64*1024*1024];
    		System.gc();
    	}
    }
    

    运行结果:在执行System.gc()时,变量placeholder还在作用域中,虚拟机自然不会回收placeholder内存。
          在这里插入图片描述

    public class TestLocalVariableTable {
    	public static void main(String[] args) {
    		byte[] placeholder = new byte[64*1024*1024];
    		System.gc();
    	}
    }
    

    改进:在placeholder作用域之外进行回收
      

    public class TestLocalVariableTable {
    	public static void main(String[] args) {
    		{
    		byte[] placeholder = new byte[64*1024*1024];
    		}
    		System.gc();
    	}
    }
    

    运行结果:仍然没有用,虽然代码离开了placeholder的作用域,但是place原本占用的局部变量表的slot还没有被其他变量复用,所以作为GC Roots一部分的局部变量表仍然保持对它的关联,所以虚拟机以为placeholder还有用,并没有进行回收。除非,人为手动的将placeholder设置为null。在《pratical java》中把“不使用的对象手动赋值为null”作为一条推荐的编码规则,但是没有解释其中具体的原因,一般读者是理解不了的。但是不建议对所有的对象都这个处理,没有必要的地方不需要有这么多的类似代码。
          在这里插入图片描述
    人为手动将不使用对象手动赋值为null:

    public class TestLocalVariableTable {
    	public static void main(String[] args) {
    		{
    		byte[] placeholder = new byte[64*1024*1024];
    		placeholder = null;
    		}
    		System.gc();
    	}
    }
    

    运行结果:
          在这里插入图片描述
    还可以通过占用placeholder在原有局部变量表中的slot。

    public class TestLocalVariableTable {
    	public static void main(String[] args) {
    		{
    		   byte[] placeholder = new byte[64*1024*1024];		
    		}
    		int a = 0;
    		System.gc();
    	}
    }
    

          在这里插入图片描述
      局部变量表对开放还有的影响有:局部变量不会被赋予初始值,所以必须初始化才能使用。局部变量不会像类变量存在准备阶段,赋予系统初始值,所以不被初始化是不能使用的。

    public class TestLocalVariableUninit {
    	public static void main(String[] args) {
    		int a;
    		System.out.println(a);
    	}
    }
    

    操作数栈 动态连接 方法返回地址

    操作数栈:操作数栈一个后进先出的栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到 Code 属性的 max_stacks 数据项中。操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值。一个变量一个solt,64为的占2个solt。java中明确64位的是long & double。

    动态连接 :在Class文件的常量池中有大量的符号引用,字节码的方法调用指令以常量池指向方法的符号引用为参数,这些符号引用一部分在类加载阶段或者第一次使用时就转换成符号引用称为静态解析,另外一部分是在每次运行期时转为直接引用,这部分称为动态连接。所以,通常说的动态连接是在运行时将常量池中的符号引用变成直接引用过程,而不是在加载阶段或者第一次使用时转换成直接引用
    方法返回地址:方法只有2中退出方式,正常情况下,遇到return指令退出。还有就是异常退出。 正常情况:一般情况下,栈帧会保存 在程序计数器中的调用者的地址。虚拟机通过这个方式,执行方法调用者的地址然后把返回值压入调用者中的操作数栈。 异常情况:方法不会返回任何值,返回地址有异常表来确定,栈帧一般不存储信息。

    方法调用

      方法的调用主要是确定被调用方法的版本,方法版本的确认有解析调用方式和分派调用方式。解析调用针对静态方法、私有方法等不可重载重写方法。分派调用与java中多态方法如何实现紧密相关。

    解析调用

      解析调用在类加载解析阶段,例如静态方法,私有方法不可重载的方法在编译时方法的版本就被确定了,符号引用转换成了直接引用,而且运行期不会被改变。这些方法都由invokestatic或invokespecial调用,都叫做非虚方法,除此之外还有一种是由final修饰的方法,虽然由invokevirtual调用,但是该方法无法被重写,所以也是非虚方法

    分派调用-----静态分派根据静态类型确定重载方法版本

       分派调用:上面说的是只有一种方法版本,接下来说的是有多个方法版本,例如重载的方法,和重写的方法,这时候就要进行方法分派。方法分派有静态分派和动态分派。java是门面向对象的语言,多态是面向对象的其中一种特征。多态跟方法分派是密不可分的,方法分派就是虚拟机确定方法的调用版本,通俗就是确认调用哪个方法。
    静态分派:静态分派是在编译期就确定了方法的调用版本,跟静态分派有关的就是方法重载。变量有静态类型,有实际类型。例如以下代码:Human是父类,Man是子类,那么Human是man这个变量的静态类型,Man是实际类型。
    Human man = new Man();
       如果一个相同的方法,参数类型不同就可以构成方法重载,那么静态分派是根据静态类型来确认重载方法的版本。例如下面这段伪代码: 类中有两个方法,代码结果是调用第一个方法。

    public void add(Human man){}
    public void add(Man man){}
    Human man = new Man();
    方法调用:add(man);
    

       静态分派是根据静态类型来确认重载方法的版本,sayHello()中具体调用哪个版本是看参数静态类型,而非实际类型。例如:

    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 guy)
        {
            System.out.println("hello,gentleman!");
        }
        public void sayHello(Woman guy)
        {
            System.out.println("hello,lady");
        }
        public static void main(String[] args)
        {
            Human man = new Man();
            Human woman = new Woman();
            StaticDispatch dispatch = new StaticDispatch();
            dispatch.sayHello(man);
            dispatch.sayHello(woman);
            /*
                如果强转了以后,类型也跟着变化了。
                静态分配的典型应用是方法重载。但是方法重载有时候不是唯一的,所以只能选合适的。
                dispatch.sayHello((Man)man);
                dispatch.sayHello((Woman)woman);
            */
        }
    }
    

    运行结果:
    在这里插入图片描述

    分派调用-----动态分派根据实际类型确定重写方法版本

       动态分派: 动态分派跟方法重写有关,动态分派的方法选择是在运行时期确定,它会根据调用对象的实际类型去确认调用方法的版本。动态分派跟invokevirtual指令有关,该指令执行有以下步骤:
    ①找到栈顶第一个元素指向对象的实际类型,记作类型C;
    ②如果在类型C中找到与常量描述符和简单名称都相符的方法,并进行权限校验,如果通过则返回这个方法的直接引用,如果不通过则抛出访问非法异常。
    ③如果没找到,则按照继承关系从下往上依次对C的父类进行搜索。
    ④如果没找到,则抛出java.lang.AbstractMethodError异常。

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

    运行结果:
    在这里插入图片描述

    单分派(动态分派)与多分派(静态分派)

       分派根据方法参数与方法的接收者关系可分为单分派和多分派。书上说,静态分派要根据静态方法进行选择具体的版本是多分派类型,动态分派是执行invokevitual指令时,唯一看实际类型决定方法版本属于单分派。

    public class Dispatch {
        static class QQ{}
        static class _360{}
        public static class Father{
            public void hardChoice(QQ qq){
                System.out.println("Father QQ");
            }
            public void hardChoice(_360 aa){
                System.out.println("Father 360");
            }
        }
        public static class Son extends Father{
            public void hardChoice(QQ qq){
                System.out.println("Son QQ");
            }
            public void hardChoice(_360 aa){
                System.out.println("Son 360");
            }
        }
        public static void main(String[] args)
        {
            Father father = new Father();
            Father son = new Son();
            father.hardChoice(new _360());
            son.hardChoice(new QQ());
        }
    }
    

    运行结果:
    在这里插入图片描述

    基于栈的字节码执行引擎

       java通常都被人定位为解释执行语言,在java初生的JDK1.0时代,这种定位还是比较准确。但是当前主流的虚拟机中都包含了即时编译器(编译执行是通过即时编译器产生本地代码执行),因此严格意义上讲Class文件中的代码是解释执行还是编译执行只有虚拟机自己才能准确判断
       先介绍有基于寄存器的指令集和基于栈的指令集。
       基于栈的指令集:先看一个1+1加法过程:两条iconst_1指令连续把两个常量1压人栈后,iadd指令把栈顶的两个值出栈、相加,然后把结果放回栈顶,最后istore_0把栈顶的值放到局部变量表的第0个Slot中。

           iconst_1
           iconst_1
           iadd
           istore_0
    

       上诉是基于栈的指令集,基于寄存器的指令集和操作单片机中的寄存器类似。基于栈的指令集 是和硬件无关的,而基于寄存器则依赖于硬件基础。基于寄存器在效率上优势。但是虚拟机的出现,就是为了提供跨平台的支持,所以jvm的执行引擎是基于栈的指令集。

    public class Demo {
        public static void foo() {
            int a = 1;
            int b = 2;
            int c = (a + b) * 5;
        }
    }
    

    直接使用命令javap查看它的字节码指令如下:

    public static void foo();
      Code:
         0: iconst_1//把操作数压入操作数栈
         1: istore_0//将操作数栈顶元素弹出保存至局部变量表中
         2: iconst_2
         3: istore_1
         4: iload_0
         5: iload_1
         6: iadd
         7: iconst_5
         8: imul
         9: istore_2
        10: return
    

    执行过程:
             在这里插入图片描述

    展开全文
  • JVM篇·虚拟机字节码执行引擎

    千次阅读 2021-07-14 09:43:12
    虚拟机字节码执行引擎 本文为《深入理解Java虚拟机_JVM高级特性与最佳实践·周志明》学习笔记 文章目录虚拟机字节码执行引擎运行时栈帧结构局部变量表操作数栈动态连接方法返回地址附加信息方法调用解析5条方法...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 34,097
精华内容 13,638
关键字:

虚拟机字节码执行引擎