《深入理解Java虚拟机》读书笔记----虚拟机字节码执行引擎

《深入理解Java虚拟机》读书笔记----虚拟机字节码执行引擎,第1张

《深入理解Java虚拟机》读书笔记----虚拟机字节码执行引擎

本篇为JVM专栏第五篇,讲解字节码相关知识。上篇是类的加载机制,还没看的可以补补课哈。

点击跳转

执行引擎是Java虚拟机核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和 *** 作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

运行时栈帧结构

方法是JVM最基本的执行单元,“栈帧”则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、 *** 作数栈、动态连接和方法返回地址等信息。一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。

一个线程中的方法调用链可能会很长,以Java程序的角度来看,同一时刻同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”,与这个栈帧所关联的方法被称为“当前方法”。执行引擎所运行的所有字节码指令都只针对当前栈帧进行 *** 作,在概念模型上,典型的栈帧结构如下图所示。

局部变量表

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

一个变量槽可以存放一个32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、float、reference和returnAddress这8种类型。

第7种reference类型表示对一个对象实例的引用,第8种returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,某些很古老的Java虚拟机曾经使用这几 条指令来实现异常处理时的跳转,但现在也已经全部改为采用异常表来代替了。

*** 作数栈

*** 作数栈也常被称为 *** 作栈,它是一个后入先出栈 。 *** 作数栈的最大深度也在编译的时候被写入Code属性的max_stacks数据项之中。 *** 作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型

当一个方法刚刚开始执行的时候,这个方法的 *** 作数栈是空的,在方法的执行过程中,会有各种字节码指令往 *** 作数栈中写入和提取内容,也就是出栈和入栈 *** 作。

动态连接

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

方法返回地址

当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这种退出方法的方式称为“正常调用完成”

另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。这种退出方法的方式称为“异常调用完成。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。 一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的 *** 作有:恢复上层方法的局部变量表和 *** 作数栈,把返回值压入调用者栈帧的 *** 作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还未涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的 *** 作之一,Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析

分派

分派调用过程体现了多态性特征,分为静态分派、动态分派

Invoke

JDK 7时新加入的java.lang.invoke包这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制称为“方法句柄”

举例:无论obj是何种类型都可以正确调用到println()方法。

~~~java public class MethodHandleTest {

static class ClassA { public void println(String s) { System.out.println(s); } }

public static void main(String[] args) throws Throwable { Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA(); // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法 getPrintlnMH(obj).invokeExact("icyfenix"); }

private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable { // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。

MethodType mt = MethodType.methodType(void.class, String.class); // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。 // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接 收者,也即this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo()方法来完成这件事情。 return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver); } } ~~~

方法getPrintlnMH()中实际上是模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上,而是通过一个由用户设计的Java方法来实现。而这个方法本身的返回值 (MethodHandle对象),可以视为对最终调用方法的一个“引用”。以此为基础,有了MethodHandle就可以写出类似于C/C++那样的函数声明了:

~~~java void sort(List list, MethodHandle compare) ~~~

这和反射相似,但是不同。

1、Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。

2、在MethodHandles.Lookup上的3个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual(以及invokeinterface)和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用ReflectionAPI时是不需要关心的。

3、Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等的运行期信息。而后者仅包含执行该方法的相关信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。

4、由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化,在MethodHandle上也应当可以采用类似思路去支持,而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5426692.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-11
下一篇 2022-12-11

发表评论

登录后才能评论

评论列表(0条)

保存