栈帧(stack frame)是一种帮助虚拟机执行方法调用与方法执行的数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
slot是存储局部变量表的最小单位 可复用 复用情况与虚拟机的具体实现有关
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析;另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为Java的多态性。
直接引用的五种情况:
- invokeinterface:调用接口中的方法,实际上是运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
- invokestatic:调用静态方法
- invokespecial:调用自己的私有方法、构造方法(<init>)以及父类的方法
- invokevirtual:调用虚方法,运行期动态查找的过程
- invokedynamic: 动态调用方法,最为复杂
public class MyTest4 {
public static void test(){
System.out.println("test invoke static");
}
public static void main(String[] args) {
test();
}
}
main方法的code
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method test:()V
3: return
在解析阶段就能确定
静态解析的四种情形
- 静态方法
- 父类方法
- 构造方法
- 私有方法(无法被重写)
以上四种方法称为非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用
通过字节码分析Java方法的静态分派与动态分派
public class MyTest5 {
// 方法重载,是一种静态行为,编译器就可以完全确定
public void test(Grandpa grandpa){
System.out.println("grandpa");
}
public void test(Father father){
System.out.println("father");
}
public void test(Son son){
System.out.println("son");
}
public static void main(String[] args) {
Grandpa g1 = new Father();
Grandpa g2 = new Son();
MyTest5 myTest5 = new MyTest5();
myTest5.test(g1);
myTest5.test(g2);
}
}
class Grandpa{
}
class Father extends Grandpa{
}
class Son extends Father{
}
输出
这是因为方法的静态分派
Grandpa g1 = new Father(); g1的静态类型是Grandpa ,而g1的实际类型(真正指向的类型)是Father。
变量的静态类型是不会发生变化的,而实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期方可确定。
public class MyTest6 {
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit orange = new Orange();
apple.test();
orange.test();
apple = new Orange();
apple.test();
}
}
class Fruit{
public void test(){
System.out.println("Fruit");
}
}
class Apple extends Fruit{
@Override
public void test() {
System.out.println("Apple");
}
}
class Orange extends Fruit{
@Override
public void test() {
System.out.println("Orange");
}
}
方法的动态分派:
方法接收者
invokevirtual字节码指令的多态查找流程
- 找到操作数栈顶的第一个元素指向的对象的实际类型
- 找出具体的方法
- 没找到就从下至上去寻找
比较方法重载(overload)与方法重写(overwrite)
方法重载是静态的,是编译期行为,方法重写是动态的,是运行期行为
虚方法表与动态分派机制详解
public class MyTest7 {
public static void main(String[] args) {
Anmial anmial = new Anmial();
Anmial dog = new Dog();
anmial.test("hello");
dog.test(new Date());
}
}
class Anmial{
public void test(String str){
System.out.println("Animal str");
}
public void test(Date date){
System.out.println("Anmial date");
}
}
class Dog extends Anmial{
@Override
public void test(String str) {
System.out.println("Dog str");
}
@Override
public void test(Date date) {
System.out.println("Dog date");
}
}
针对于方法的动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table, vtable),
针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table,itable)
基于栈的指令集与基于寄存器的指令集详细对比
现代JVM在执行Java代码的时候,通常会将解释执行与编译执行二者结合起来进行。
所谓解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令
所谓编译执行,就是通过即时编译器(Just In Time,JIT)将字节码转换为本地机器来执行,现代JVM会根据代码热点来生成相应的本地机器码
基于栈的指令集与基于寄存器的指令集之间的关系
- JVM执行指令时所采取的方式是基于栈的指令集
- 基于栈的指令集主要的操作有入栈与出栈两种
- 基于栈的指令集的优势在于它可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构紧密关联的,无法做到可移植
- 基于栈的指令集的缺点在于完成相同的操作,指令数量通常比基于寄存器的指令集数量要多,基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中执行的,速度要快很多。虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些
public int myCaculate();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=6, args_size=1 往下会有22个指令
0: iconst_1 将数字1推送到栈中
1: istore_1 将数字1弹出,放入索引为1的局部变量表中 局部变量表的0位置是this
2: iconst_2 将数字2推送到栈中
3: istore_2 将数字2弹出,放入索引为2的局部变量表中
4: iconst_3 将数字3推送到栈中
5: istore_3 将数字3弹出,放入索引为3的局部变量表中
6: iconst_4 将数字4推送到栈中
7: istore 4 istore_1,2,3是简写,istore 4是完整写法
9: iload_1 将索引位置1的值压入栈
10: iload_2 将索引位置2的值压入栈
11: iadd 从栈中弹出两个值,执行加法,把结果压入栈
12: iload_3 将索引位置3的值压入栈
13: isub 从栈中弹出两个值,执行减法,把结果压入栈
14: iload 4 将索引位置4的值压入栈
16: imul 从栈中弹出两个值,执行乘法,把结果压入栈
17: istore 5 将栈顶弹出,放入索引5的局部变量表中
19: iload 5 将索引位置5的值压入栈
21: ireturn 弹出栈顶值,如果操作数栈中还有其他值就丢掉
LineNumberTable:
line 10: 0
line 11: 2
line 12: 4
line 13: 6
line 15: 9
line 17: 19
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/yshuoo/jvm/bytecode/MyTest8;
2 20 1 a I
4 18 2 b I
6 16 3 c I
9 13 4 d I
19 3 5 result I
}