前言
在之前的JVM之Class文件中已经对class文件的结构有了一个大致的认识,其中有一个Code属性没有详细的讨论,这里就详细的讨论一下这个Code属性以及执行引擎和它的关系
概述
class文件经过加载后会在方法区形成一个该类的一些数据结构(或者说是Java虚拟机规定的格式),然后执行引擎会在方法区中找到main方法code属性中的指令,并且去执行它。执行引擎会维护一个虚拟机栈,虚拟机栈中有栈帧,栈帧中维护者操作数栈、局部变量表和指向该栈帧所属的方法在运行时常量池中的引用
用一个图大概描述一下他们之间的关系
运行时栈帧的结构
局部变量表
局部变量表是一组以Slot为单位的存储空间,它用来存储方法的参数和方法内部定义的变量,它在一个方法创建的栈帧中
操作数栈
操作数栈里面存放的可以是任意的Java数据类型,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈/出栈操作
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用就是为了支持动态链接,在常量池中存在大量的符号引用,这些符号引用有一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析,另外一部分将在每一次运行期间转化为直接引用,这部分称为动态链接
方法返回地址
每执行一个方法的时候,就会创建一个新的栈帧,当方法执行完之后,该栈帧就会被弹出。有两种方法可以退出一个方法,一是执行引擎遇到方法返回的字节码指令(如: ireturn),二是方法在执行的过程中遇到了异常,并且这个异常没有在方法体内得到处理。无论采用何种方式退出,在方法退出之后,都需要返回到方法被调用的位置,程序才能执行,方法返回时可能需要在上一个栈帧(调用当前方法的方法所处的栈帧)中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,调用者(上层方法)的PC计数器的值可以作为返回地址。
附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息
执行引擎一天的工作
新的一天开始了,生活依旧是那么美好,因为每天都会有不同的字节码指令等待着执行引擎去执行,早上八点,执行引擎来到了公司,等待着Java虚拟机老大分配任务,今天的任务非常的简单,就是执行一个简单的加法运算指令,这一看就是刚学程序的小白写的。一个类被编译成class文件之后,它的一生会经历下面的过程
执行引擎深知自己的岗位就是在class文件使用阶段,也就是执行字节码指令的时候,这个时候是在class文件正值壮年的时候,Java虚拟机老大掌控着class文件的一生,就像上帝一样。下面看看,Java虚拟机今天接的是一个什么样的Java代码:
源码:
package com.blog.test;
public class EngineTest {
public static void main(String[] args) {
sum(1,2);
}
private static int sum(int i, int j) {
return i+j;
}
}
字节码文件中的main方法和sum方法(其中有该方法的内容,也就是字节码指令,该指令被存到方法的code属性里了):
main方法的栈帧内容:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_1 // 将常量1推送至栈顶(main栈帧中的操作数栈)
1: iconst_2 // 将常量2推送至栈顶
2: invokestatic #16 //调用静态方法 sum 创建新的栈帧 --> Method sum:(II)I
5: pop // 将操作数栈顶的元素弹出
6: return // 无返回值得返回
LineNumberTable:
line 6: 0
line 7: 6
LocalVariableTable: // 局部变量表(main函数栈帧中的)
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
sum方法的栈帧内容
public static int sum(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0 // 将本地变量表中的第一个 int 型本地变量推送至操作数栈顶(下标从从0开始)
1: iload_1 // 将第二个 int 型本地变量推送至栈顶
2: iadd // 将操作数栈顶的两个元素弹出,相加,并且结果压入操作数栈
3: ireturn// 结束方法,返回一个 int 型数据
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 i I
0 4 1 j I
虚拟机老大将任务分解,交给各个部门去做,各个部门依次开始工作(各个部门通常是相互交叉地进行,比如说在执行的时候遇到一个指令要调用另一个类的一个方法,这个时候如果另一个类未被解析,这个时候会触发解析),前面的工作做得差不多了,到了执行这一步,执行引擎去方法区拿到该类的字节码看了看,没什么难度,然后依次执行了,执行过程如下图:
执行引擎拿到 iconst_1 指令后,不假思索地将给操作数栈压入了int 型的 1,因为他把Java虚拟机规范已经背的滚瓜烂熟了,所有的指令都要遵循Java虚拟机规范中规定的语意。当他遇到 invokestatic 这条指令时,眉头一皱,这个指令的含义非常简单,就是调用一个静态方法,但是这个指令的语义不是那么好实现的,挺麻烦的,具体要干什么?
①他要pop出当前栈帧(main方法栈帧)中操作数栈中的X个元素
②将这X个元素倒叙排列
这X个元素是下一个栈帧(也就是被调用的函数)所需要的元素(参数列表所需),这里就是1和2,因为下一个函数要用到这两个数
③将倒叙排列的元素放入局部变量表
也就是把1(最后pop出)放到第一个局部变量表中,把2放入第二个局部变量表中
最后设置下一个栈帧为当前栈帧,开始执行。这一大步走完,很是累啊。执行引擎执行完了,歇了一会,继续工作,如下:
这些指令相当轻松一些,很快就执行完了,但是sum函数这个栈帧还没有完,还有最后一个指令 : ireturn, 这个指令语意也不是很难,就是:结束方法,返回一个 int 型数据,但是实现起来也不容易啊,还要干很多活,要实现这个指令,执行引擎还需要做:
①得到上一个栈帧(main栈帧)
②把自己栈帧中操作数栈顶的元素pop出来
③压入上一个栈帧的操作数栈中
之后当前栈帧(sum栈帧)pop出去,PC指向main函数中 invokestatic 指令的下一条指令(pop),这时,main函数栈帧又变成了当前栈帧了,然后执行引擎继续工作了,直到main函数中没有指令可执行了。
执行过程如下:
执行引擎忙活了一天,总算忙完了,看似简单的指令,要弄完还要费不少功夫呢。好了,收拾东西回家。
总结
在class文件中保存了类的一些信息,其中有方法的一些信息,方法信息中有code属性,code属性中有执行引擎执行的字节码指令。执行引擎执行这些字节码指令,这些指令是class文件中最具有“活力”的东西。