字节码执行引擎
字节码的执行
- 解释执行
- 编译执行
- JIT编译与解释混合执行
flowchart TD A[方法调用] --> B{已编译?} B -->|是| C[执行编译后的机器码] B -->|否| D[方法调用计数器加1] D --> E{计数是否超过阈值?} E -->|否| F[解释方式执行] E -->|是| H[提交编译请求] H --> I[编译器] I --> J[后台执行编译] J --> K[Code Cache] K --> C C --> 方法返回 F --> 方法返回
这样就造成机器在热机所承载的负载可能会比冷机的高
- -Xmixed 默认为混合模式
- -Xint 解释模式
- -Xcomp 纯编译模式
热点代码检测
- 多次被调用的方法 方法计数器
- 多次被被调用的循环 循环计数器
还有一种新的编译方式,即所谓的 AOT(Ahead-of-Time Compilation),直接将字节码编译成机器代码,这是 graalvm 所做的
运行时栈帧结构
用于支持虚拟机进行方法调用和方法执行背后的数据结构
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
每个栈帧的大小在编译期就已经确定(根据局部变量表 操作数栈等)
局部变量表
用于存放方法参数和方法内部定义的局部变量,方法的Code属性max_locals数据项确定了局部变量表的最大容量
规范说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据
虚拟机实现至少都应当能通过reference做两件事:
- 根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引
- 根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间
局部变量表中的变量槽是可以重用的,但是这种重用会影响到垃圾回收,如果垃圾收集器发现变量表还存在某个变量的引用 就不会轻易回收它:
byte[] bytes = new byte[1024 * 1024 * 128];bytes = null; // 不加上这行 bytes不会在gc被调用时被回收System.gc();
操作数栈
操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中
用来支持各种指令操作
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
方法返回地址
方法执行后 有两种方式退出方法:
- 执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者
- 方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理
方法退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中
附加信息
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息
方法调用
方法调用是最普遍、最频繁的操作之一
方法调用的指令:
- invokestatic:用于调用静态方法。
- invokespecial:用于调用实例构造器`
()`方法、私有方法和父类中的方法 - invokevirtual:用于调用所有的虚方法
- invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本, 这些方法被称为非虚方法
public static void say(){ System.out.println("hello world");}public static void main(String[] args) { say();}
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=0, locals=1, args_size=1 0: invokestatic #21 // Method say:()V 3: return LineNumberTable: line 12: 0 line 13: 3
分派
静态类型:编译时能确定的类型
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载
编译过程中,编译器会暂时用符号引用来表示目标方法,这些符号引用存在类常量池中
static void f(Object obj){ System.out.println("obj");}static void f(CharSequence seq){ System.out.println("seq");}static void f(String str){ System.out.println("str");}public static void main(String[] args) { Object obj = "str"; CharSequence seq = "str"; String str = "str"; f(obj); // obj f(seq); // seq f(str); // str}
动态分派
方法覆写的话会在运行时动态决定应该调用哪个方法
invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,只有方法才会参与多态 字段没有invokevirtual相关指令 所以字段不会多态
单分派与多分派
- 静态多分派:根据参数来决定
- 动态单分派:根据被调用的对象决定
虚拟机动态分派的实现
为了性能 JVM不会频繁搜索类元数据 而是使用了一个虚方法表:
动态类型语言支持
- invokedynamic指令,用以支持动态语言的方法调用,它将调用点(CallSite)抽象成一个 Java 类,并且将原本由 Java 虚拟机控制的方法调用以及方法链接暴露给了应用程序
动态类型语言:类型检查的主体过程是在运行期而不是编译期进行的
虚方法调用对性能的会影响 JVM 使用内联缓存加速动态绑定
java.lang.invoke
模拟invokedynamic:
// 返回值类型 参数类型MethodType methodType = MethodType.methodType(void.class, String.class);Object obj = System.out;MethodHandle methodHandle = MethodHandles.lookup() .findVirtual(obj.getClass(), "println", methodType).bindTo(obj);methodHandle.invoke("hello world");
MethodHandle是在模拟字节码层次的方法调用 反射API只能为Java服务 使用invoke包可以用来开发动态语言
实战 自己控制方法调用
static class GrandFather { void thinking() { System.out.println("i am grandfather"); }}static class Father extends GrandFather { void thinking() { System.out.println("i am father"); }}static class Son extends Father { void thinking() { // 在这里填入适当的代码(不能修改其他地方的代码) // 实现调用祖父类的thinking()方法,打印"i am grandfather" try { MethodType mt = MethodType.methodType(void.class); Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); lookupImpl.setAccessible(true); MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class); try { mh.invoke(this); } catch (Throwable throwable) { throwable.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } } }
执行引擎
- 解释执行
早期的JVM是通过解释字节码来执行的 但后面也出现了编译为本地机器码的编译器 所以现代的Java是拥有编译模式以及解释模式亦或者混合模式
- 基于栈
对于JVM来说 基于栈移植容易 实现容易
栈架构指令集的缺点在于速度稍慢
实战:分析一段简单的四则运算
public int calc(){ int a = 100; int b = 200; int c = 300; return (a + b) * c;}
public int calc(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: bipush 100 2: istore_1 3: sipush 200 6: istore_2 7: sipush 300 10: istore_3 11: iload_1 12: iload_2 13: iadd 14: iload_3 15: imul 16: ireturn LineNumberTable: line 9: 0 line 10: 3 line 11: 7 line 12: 11
javap提示这段代码需要深度为2的操作数栈和4个变量槽的局部变量空间
1.Bipush指令的作用是将单字节的整型常量值(-128~127)推入操作数栈顶 参数为100
2.istore_1指令的作用是将操作数栈顶的整型值出栈并存放到第1个局部变量槽中 后面的istore 以及xxpush都是一样
3.iload_1指令的作用是将局部变量表第1个变量槽中的整型值复制到操作数栈顶
4.load_2指令的执行过程与iload_1类似,把第2个变量槽的整型值入栈
5.iadd指令的作用是将操作数栈中头两个栈顶元素出栈,做整型加法,然后把结果重新入栈
6.iload_3指令把存放在第3个局部变量槽中的300入栈到操作数栈中
7.指令imul是将操作数栈中头两个栈顶元素出栈,做整型乘法,然后把结果重新入栈,与iadd完全类似
8.ireturn指令是方法返回指令之一,它将结束方法执行并将操作数栈顶的整型值返回给该方法的调用者