概述

  Java 虚拟机规范规定了字节码执行引擎的规范,这个规范形成了所有具体虚拟机实现的统一外观:输入字节码,内部解析,输出结果。
  执行引擎的运行其实就是栈帧的入栈和出栈的过程。每个栈帧都包括了 局部变量表 (local variable table,也称本地变量表)操作数栈 (operand stack)栈帧信息 (动态连接,方法返回地址和其他一些额外信息)


栈帧结构

局部变量表

  用于存储入参和内部定义的局部变量,当代码被编译成 Class 文件的时候,方法的 Code 属性的 max_locals 就已经定义了局部变量表的最大容量。(Code 为方法的元信息和方法体)
  局部变量以变量槽 (variable slot,简称 slot) 为单位,Java 虚拟机规范中规定一个 slot 为 32 位,但也有一些虚拟机使用 64 位的 slot。slot 用来存放虚拟机基本类型的数据 (boolean, int, double, reference …)。如果是 double 或是 long 这种 64 位长的数据类型,就使用两个 slot 来存放,访问时必须两个 slot 一起访问。
  每个栈帧都至少含有一个 slot(用来存放 this)。

操作数栈

  和局部变量表一样,操作数栈的最大深度在编译时就已经确定 (由 Code 属性的 max_stacks 确定)
  操作数栈并不是什么很高深的东西,它的原理很简单,接触过数据结构的人应该都知道,就是一个用来计算加减乘除的数据结构而已 (将表达式转化为后缀表达式,读到数就将数压到栈中,读到符号就从栈中去两个数来计算,再压入栈中,如 1 + 3 就是 1 3 +,读到 1 和 3 的时候将它们压入栈中,读到 + 的时候将 1 和 3 读出来相加,再将结果 4 压入栈中)
  实际的操作数栈比上面讲的要复杂点,因为虚拟机一般会加入一些优化,如大多数虚拟机会将操作数栈数据重叠的地方共享,这样可以避免数据的复制。

动态连接

  解析阶段的时候会将一部分的符号引用转换为直接引用,而剩下的一些符号引用需要在运行时才能转换,这部分就被称为动态连接。

方法返回地址

  执行一个方法的时候只有两种方式可以退出:正常方法出口和异常方法出口。而在方法退出的时候会在栈帧中保存方法返回的地址,方便程序继续执行。正常方法出口的方法返回地址一般是程序计数器最后记录的地址;异常方法出口由异常表来决定。


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

  字节码执行引擎有解释执行引擎和编译执行引擎 (通过 JIT)。现在讲的是解释执行引擎,Java 虚拟机最基本的引擎。
  如下图可以看到从代码到执行的过程中在 抽象语法树 那里有个分叉,中间那一条则是解释执行 (Java 等运行在虚拟机上的语言);下面那一条就是编译执行 (C / C++ 等)
JVM 篇:虚拟机字节码执行引擎
  Java 语言的执行使用的是半独立的编译器 (从程序源码到抽象语法树,再到指令流的过程,如 javac)解释器 则在 Java 虚拟机内部。

基于栈的指令集

  基于栈的指令集是相对于基于寄存器的指令集。

  • 基于栈的指令集指令更加紧凑,并且因为和具体的硬件环境无关所以可以移植。缺点是指令相对来说更多,并且因为栈是基于内存的所以也导致访问内存的次数更多,速度较慢。(虚拟机一般通过栈顶缓存将频繁使用的指令映射到寄存器中来加快速度)

  以上内容为阅读 深入理解Java虚拟机(第2版)后的笔记及对 JDK8 的实践补充。看完这本书后最大的感觉就是,,,再看一遍,很多原来理解不了的知识点就可以看懂了,因为很多内容是前后呼应的。有兴趣的可以去阅读这本书,强推。

相关文章: