Java虚拟机执行引擎:字节码输入 - 字节码解析 - 执行结果
运行时栈帧结构
什么叫栈帧?
存储了方法的局部变量表,操作数,动态连接和方法返回至地址等信息,用于支持虚拟机进行方法调用和执行的数据结构,每一个方法从调用开始至完成,都对应一个栈帧在虚拟机中从入栈到出栈的过程
对于多个线程,只有栈顶的栈帧才是有效的,与这个栈帧相关的方法称为当前方法

- 局部变量表
存放方法参数和方法内部定义的局部变量,容量由Code属性中的max_locals确定 已变量槽(slot)为最小单位,但并没有确定一个slot占用多大的空间,允许slot的长度随着处理器与操作系统或虚拟机的不同变化,一个slot可以存放一个32位以内的数据结构,包括:boolean byte char short int float refrence(对一个对象实例的引用) returnAddress
当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用,即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量; 为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。当离开了某些变量的作用域之后,这些变量对应的 Slot 就可以交给其他变量使用。这种机制有时候会影响垃圾回收行为public class Demo{ public static void main(String[] args){ { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 1; System.gc(); // 如果这里不加int a = 1,则不会触发系统的gc } }这是因为代码一中 placeholder 虽然离开了作用域,但之后没有任何局部变量对其进行读写,也就是说其占用的 Slot 没有被复用,也就是说 placeholder 占用的内存仍然有引用指向它,因而它没有被回收。而代码二中的变量a由于复用了 placeholder 的 Slot ,导致 placeholder 引用被删除,因此占用的内存空间被回收 所以推荐在对象使用完后,手动将对象设为null值 局部变量的变量系统是不会初始化的,所以如下会报错
int a; System.out.print(a); -
操作数栈
一个先入后出栈,最大深度由Code属性的max_stacks决定
当一个方法刚开始时,方法的操作数栈是空的,需要在执行中各个字节码指令往操作数栈中写入和提取(出栈/入栈)
eg:相加操作:iadd,运行时会将两个int出栈相加,然后将结果入栈 -
动态连接
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析(对应上一章的类加载:解析)。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接 - 方法返回地址
方法在遇到任意返回字节码指令or遇到异常(且没有在内部处理)时会退出;
方法退出后,需要返回到方法被调用的位置,相当于把当前栈帧出栈
方法调用
方法调用唯一任务是确定被调用方法的版本,但不涉及运行过程。
- 解析
上面我们了解到,在类加载的解析截断,只会有一部分符号引用转化为直接引用,前提是什么呢?
只有放在在真正运行前就有一个可确定的调用版本,并且在运行期不可改变,在编译器就能确定下来。
满足这个要求,只有静态方法和私有方法两大类,因为前者和类型关联,后者不允许外部访问,导致这两个都不能通过继承或其他方式重写(包括静态方法、私有方法、实例构造器、父类方法,这些称为非虚方法,包括final方法)public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=1, args_size=1 0: invokestatic #4 // Method StaticMethod:()V 3: return LineNumberTable: line 24: 0 line 25: 3这里的static方法的字节码的确是通过invokestatic调用的
- 静态分派
class Man extends Human{} Human man = new Man();这里的Human称为变量的静态类型,Man称为变量的实际类型;
静态类型和实际类型都可能发生变化,不同是静态类型变化发生在使用时,变量本身的静态类型不会被改变,且最终静态类型在编译器可知;实际类型的结果需要在运行期才能确定Human man = new Man(); Human woman = new Woman(); sayHello(man); sayHello(woman); // 这里实际选择sayHello(Human human)为调用目标,因为javac会根据参数的静态类型决定使用哪个重载版本总结:所有依赖静态类型定位方法执行版本的分派动作称为静态分派,典型应用是方法重载
- 动态分派
public class Stack{ public abstract class Human{ public void sayHello(){ System.out.println("Human sayHello"); } } public class Man extends Human{ public void sayHello(){ System.out.println("Man sayHello"); } } public class Woman extends Human{ public void sayHello(){ System.out.println("Woman sayHello"); } } public static void main(String[] args){ Stack st = new Stack(); Human man = st.new Man(); Human woman = st.new Woman(); man.sayHello(); woman.sayHello(); } }stack=4, locals=4, args_size=1 0: new #2 // class Stack 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: new #4 // class Stack$Man 11: dup 12: aload_1 13: dup 14: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class; 17: pop 18: invokespecial #6 // Method Stack$Man."<init>":(LStack;)V 21: astore_2 22: new #7 // class Stack$Woman 25: dup 26: aload_1 27: dup 28: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class; 31: pop 32: invokespecial #8 // Method Stack$Woman."<init>":(LStack;)V 35: astore_3 36: aload_2 37: invokevirtual #9 // Method Stack$Human.sayHello:()V 40: aload_3 41: invokevirtual #9 // Method Stack$Human.sayHello:()V 44: return上图0-25是准备工作,为man和woman分配空间;
36-41是关键部分,首先36、40将实例化的两个对象压到栈顶;37、41是方法调用指令,可见调用者(所有者,也成为接收者)都是Human,然而实际执行的目标方法不同,这里就要介绍一下invokevirtual指令的解析过程了:
1.找到栈顶第一个元素指向的对象的实际类型C
2.如果在类型C中找到与常量的描述符和简单名称一致的方法,则进行访问权限校验,如果通过则返回这个方法的引用,如果不通过,返回IllegalAccessError异常
3.如果没有,则按照继承关系对C的各个父类进行第2步的搜索和验证
4.如果始终没有,则抛出AbstractMethodError异常
总结:在运行期根据实际类型确定方法执行版本,典型应用是重写 -
单分派与多分派
单分派:根据一个宗量的类型进行方法的选择称为单分派
多分派:根据多于一个宗量的类型对方法的选择称为多分派
宗量:方法的接收者与方法的参数统称为方法的宗量
当前java在编译阶段,既要看静态类型,又要看方法参数,所以静态分派属于多分派类型;
由于编译器已决定目标方法签名,所以此时虚拟机只考虑接收者的实际类型,故动态分派属于单分派类型 - 虚拟机动态分派的实现
通过为类在方法区建立虚方法表(vtable),invokeinterface执行时使用接口方法表(itable)
使用虚方法表索引代替元数据查找提高性能
这里只要子类没有重写父类的方法,那么子类的虚方法表里的地址入口和父类的地址入口一致;
方法表一般在类加载的连接阶段初始化
动态类型语言支持
什么是动态语言?动态类型语言的特征是类型检查的主体过程是在运行期而非编译期
- java.lang.invoke包
通过MethodHandle进行方法调用一般需要以下几步: (1)创建MethodType对象,指定方法的签名; MethodHandles.Lookup lookup = MethodHandles.lookup(); (2)在MethodHandles.Lookup中查找类型为MethodType的MethodHandle; MethodType type = MethodType.methodType(String.class, int.class, int.class); MethodHandle mh = lookup.findVirtual(JComboModel.class, "substring", type); (3)传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。 mh.invoke(combo, new CustomComboModel());
指令:invokedynamic
首先先回顾一下java虚拟机调用方法的字节码指令
invokestatic //调用静态方法
invokespecial //调用私有方法、实例构造器方法、父类方法
invokevirtual //调用实例方法
invokeinterface//调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic //调用动态方法
前面4个除了invokestatic都有一个共同点,就是需要有方法的接收者
interface SampleInterface {
void sampleMethodInInterface();
}
class Sample implements SampleInterface {
public void sampleMethodInInterface() {}
public void normalMethod(String name) {}
public static void staticSampleMethod() {}
}
public void invoke() {
SampleInterface sample = new Sample();
sample.sampleMethodInInterface();
Sample newSample = new Sample();
newSample.normalMethod();
Sample.staticSampleMethod();
}
下面是Class字节码
public void invoke();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class Sample
3: dup
4: invokespecial #3 // Method Sample."<init>":()V 对应SampleInterface sample = new Sample();
7: astore_1
8: aload_1
9: invokeinterface #4, 1 // InterfaceMethod SampleInterface.sampleMethodInInterface:()V 对应sampleMethodInInterface();
14: new #2 // class Sample
17: dup
18: invokespecial #3 // Method Sample."<init>":()V 对应Sample newSample = new Sample();
21: astore_2
22: aload_2
23: invokevirtual #5 // Method Sample.normalMethod:()V 对应newSample.normalMethod();
26: invokestatic #6 // Method Sample.staticSampleMethod:()V 对应Sample.staticSampleMethod();
29: return
从上述可以看出,invokestatic,invokespecial方法选择是固定的;invokevirtual,invokeinterface虽然方法选择不固定,但是可根据接收者实际类型选择
invokedynamic
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public void useConstantCallSite() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh = lookup.findVirtual(String.class, "substring", type);
ConstantCallSite callSite = new ConstantCallSite(mh); // 获取CallSite对象,与动态调用点关联
MethodHandle invoker = callSite.dynamicInvoker(); // 调用跟CallSite关联的MethodHandle指向方法
String result = (String) invoker.invoke("Hello", 2, 3);
}
1:通过MethodHandles.lookup()获取
2:获取方法类型MethodType.methodType(Class<?> rtype, Class<?>[] ptypes)
3:通过方法名,类获取方法句柄
findStatic()、findVirtual()、findSpecial()对应的就是静态/实例/构造or父类or私有方法 - findStatic(Class<?> refc, String name, MethodType type)
4.根据方法句柄获取callSite对象
5.通过callSite.dynamicInvoker()获取方法句柄
4.方法句柄通过invoke执行方法
下面研究一下字节码
1:0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
#2 = InvokeDynamic #0:#19 // #0:run:()Ljava/lang/Runnable;
0 为占位符,hotspot用不到
2:#2 = InvokeDynamic #0:#19 // #0:run:()Ljava/lang/Runnable;
#0 代表引导方法取BootstrapMethods第0项
#19 = NameAndType #24:#25 // run:()Ljava/lang/Runnable; 从这个常量可以获取方法名和描述符
3:BootstapMethod
BootstrapMethods:
0: #16 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#17 ()V
#18 invokestatic TestClass.lambda$main$0:()V
#17 ()V
调用MethodHandle$Lookup的findStatic()方法 - 产生MethodHandle - 创建CallSite
基于栈的字节码解释执行引擎
主要探索虚拟机如何执行方法中的字节码指令
java虚拟机的执行引擎分为:解释执行 and 编译执行
-
解释执行 javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树、遍历语法树生成线性字节码指令流的过程。
- 基于栈的指令集与基于寄存器的指令集
java编译器输出的指令流基本是基于栈的指令集架构
基于栈的指令集:(计算1+1)iconst_1 - iconst_1 - iadd - istore_0 连续将常量1压栈 - iadd指令将栈顶的值出栈相加 - 将结果放回栈顶(istore_0将栈顶的值放到局部变量表第0个slot中)基于寄存器:(计算1+1)
mov eax, 1 - add eax, 1 mov将EAX寄存器值设1 - add将这个值+1 - 结果保存在EAX寄存器内 - 基于栈的解释器执行过程
public int calc(){ int a = 100; int b = 200; int c = 300; return (a + b) * c; } public int calc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 操作数栈深度2,局部变量空间4个slot 0: bipush 100 将单字节的整形常量推到栈顶 2: istore_1 将栈顶的值放到第1个局部变量slot中 3: sipush 200 6: istore_2 7: sipush 300 10: istore_3 11: iload_1 将第1个局部变量的值复制到栈顶,此时栈顶 100 12: iload_2 将第2个局部变量的值复制到栈顶,此时栈顶 200 - 100 13: iadd 相加,将结果推到栈顶,此时栈顶 300 14: iload_3 将第3个局部变量的值复制到栈顶,此时栈顶 300 300 至此为止,操作数栈深度的确为2 15: imul 相x,此时栈顶 90000 16: ireturn
