1、概述
虚拟机和物理机是相对应的,这两种机器都具有代码执行能力。物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统上的,而虚拟机的执行引擎是自己实现的,可以自行定制指令集和体系结构。
2、运行时栈帧结构
栈帧是虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈中的栈元素。栈帧的概念结构如下:
在编译期,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,栈帧分配的内存不会受到程序运行期变量数据的影响,只取决于虚拟机实现。
只有位于栈顶的栈帧才是有效的,称为当前栈帧,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
2.1局部变量表
局部变量表是一个变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Slot)为最小单位,Slot的大小未知,但是规范中说一个Slot能存放一个boolean、byte、char、short、int、float、reference、returnAddress,long和double则需要两个Slot的空间。
在方法执行的时候,虚拟机通过局部变量表完成参数值到参数变量列表的传递过程,如果是实例方法,那么第0个Slot用于传递this引用,其他参数按照参数列表顺序排列。
2.2 操作数栈
操作数栈中的元素是java数据类型,在方法执行过程中,如果遇到数据操作,就会将待操作的数据进行出栈和入栈操作。例如计算3+2时,3和2就会在操作数栈的栈顶,当指令iadd运行的时候就将3和2出栈,然后相加,再将结果5入栈。
2.3 动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用就是为了支持方法调用过程中的动态连接。
字节码中的方法调用指令以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段就转化为直接引用,这叫静态解析。另一部分将在运行期间转化为直接引用,这叫动态连接。
2.4 方法返回地址
方法退出后,都需要返回到方法被调用的位置。
3、方法调用
方法调用阶段唯一关心的就是调用哪一个方法,而不是具体怎么执行。一切方法调用在Class文件里面存储的都只是符号引用,而不是在实际运行时内存布局中的入口地址(直接引用)。
3.1 解析
所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转化为直接引用,这种静态解析的前提是:待调用的方法在编译期就必须确定下来。
静态方法、私有方法、实例构造器、父类方法这4类方法在类加载的时候就会把符号引用转化为直接引用。它们是通过invokestatic和involespecial指令调用的。这两种调用方式是静态的,在编译期间就能完全确定。
3.2 分派与多态
(1) 静态分派 (也叫编译时多态,和方法重载有关)
举一个例子,假设Man和Woman都继承了Human,
那么Human man = new Man();这个语句中,对象man的静态类型是Human,实际类型是Man。变量的静态类型是不会变的,而实际类型可能会以多态特性改变。
下面有两个方法:
public void sayHello(Human man){
System.out.println("hello guy");
}
public void sayHello(Man man){
System.out.println("hello gentleman");
}
如果对man执行sayHello(man);
会打印hello guy,程序把man当作Human类型进行方法调用了。
原因: 编译器在重载的时候是通过参数的静态类型作为判断依据的,静态分派发生在编译阶段,和虚拟机无关。
(2) 动态分派 (也叫运行时多态,和方法重写有关)
String s1 = "ab";
Object o = s1 +"c";
String s = "abc";
boolean b = o.equals(s);
上述代码结果为true,因为o的实际类型是String,因此可以调用String的equals方法并比较内容。
JAVA语言支持静态的多分派和动态的单分派。
在静态分派中,运行之前编译器不知道man的实际类型是什么,但是man是作为参数传递给其他方法的,但是代码都这样写了,总得把man安排一下吧,万一运行期间发现没有方法可执行就完了,因此编译器为了保险起见只能以它的静态类型为准,将它传递到类型匹配的方法中。将man作为参数调用sayHello()方法时,其实就是多分派,因为在运行之前必须为man选择一条稳妥的去向,要不然如果运行期间man现出真面目之后发现没有一个方法和它匹配就尴尬了,编译器选择了静态类型去安排man。
在动态分派中调用o.equals()方法的时候,编译器还是不知道o的实际类型,但是这次是o调用自己的方法,是自己做决定,而不是作为参数参入别的方法,因此可以放到运行期间再去决定用哪种类型(万一它有自己的方法呢,还是先别随意安排吧),o的实际类型是String,String类中重写了Object的equals()方法,这样o有自己的equals方法,就不会去调用Object的方法了,如果String中没有重写,还是会调用Object的方法的。
4、基于栈的解释器执行过程
public class Main {
public static void main(String[] args) {
Main m = new Main();
int c = m.add();
}
public int add(){
int a = 5;
int b = 6;
return a+b;
}
}
查看字节码指令为:
public static void main(java.lang.String[]);
Code:
0: new #2 // class Main
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method add:()I
12: istore_2
13: return
public int add();
Code:
0: iconst_5
1: istore_1
2: bipush 6
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: ireturn
}
可见 4: invokespecial #3
处调用了Main类的默认构造器<init()>
9: invokevirtual #4
调用了add()方法
在add方法中,当 int 取值 -1~5 时,JVM 采用 iconst 指令将常量压入栈中,因此iconst_5将5压入操作栈中
istore_1将5出栈并保存到局部变量表的编号为1的Slot中,编号为0的Slot是this引用
bipush将6压入操作栈中
istore_2将6出栈并保存到编号为2的Slot上
iload_1将Slot1上的数(5)复制到操作栈顶
iload_2将Slot2上的数(6)复制到操作栈顶
iadd将栈顶的两个数5和6相加
ireturn将栈顶的数11返回给方法的调用者
执行这些指令的时候,主要是 程序计数器、局部变量表、操作栈 在发生变化,
程序计数器会累加指令的总偏移量
局部变量表用于存放局部变量
操作栈用于对数据进行操作,运算指令都是对栈顶数据进行操作的。