JVM 字节码执行引擎
JVM 作为字节码与操作系统之间的中间件,实现了 java 的跨平台性。
JVM 输入的是字节码文件,执行引擎(Execution Engine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
1. JVM 运行时数据结构
详情看 [JVM 内存结构总结]。
2. 方法调用
方法调用是指确定方法的版本,不涉及方法内部的执行过程。
一切方法调用在 .class 文件里存储的都只是符号引用,这是需要在类加载期间或者是运行期间,才能确定为方法在实际 运行时内存布局中的入口地址(相当于类加载中解析阶段中把符号引用替换为直接引用)。
2.1 类加载中对方法调用的解析
类加载的解析,是对 class 文件中的符号引用转化为 JVM 内存结构中的直接引用。
JVM 中方法调用字节码的指令:
- invokestatic: 调用静态方法
- invokespecial: 调用实例构造器方法、私有方法、父类方法
- invokevirtual: 调用所有的虚方法:可以被重写的方法
- invokeinterface:调用接口方法
- invokedynamic: 用来实现动态语言特性。先在运行时动态解析出点限定符锁引用的方法,然后执行该方法。
2.2 方法的分派调用
分派是多态的体现。体现在方法的重载与重写上。
List<Integer> list = new ArrayList();
List
是静态类型,ArrayList
是实际类型。
- 静态类型在编译期确定
- 实际类型在运行时才确定
2.2.1 静态分派
静态分派是指依赖静态类型来绑定方法的直接引用,的分派行为,在编译阶段完成。
- 静态分派应用于方法的重载调用。
区分被重载的函数取决于方法签名。
- 方法名
- 参数类型
- 参数个数
被重载函数的调用时,方法参数类型取决于静态类型,即编译器即确定了方法参数类型。
2.2.2 动态分派
动态分派是指,在运行时,根据实际类型确定方法版本的分派过程。
- 应用是方法的重写。
若类 A 的实际类型是子类,则调用类 A 的重写方法,是通过动态分派来确定的。
动态分派的实现
动态方法是利用了 JVM 方法区中的 虚方法表 Virtual Method Table, 存放各个虚方法的实际入口地址。
- 若子类方法没有重写父类方法,则指向父类的实现。
- 若子类方法重写了父类方法,则指向子类方法。
虚方法表一般在类加载的连接阶段进行初始化。
3. 代码编译与字节码执行过程
Java 的编译期有两个阶段:
- 静态编译阶段:前端编译器把 java 文件编译成 .class 字节码文件;
- 动态编译阶段:JIT 编译器把字节码编译成机器码;
其中第二阶段不是必须的,它是JVM对热点代码作出的一些优化。
字节码解释器 interpreter
解释器采用逐行解释的操作,当一条字节码指令被解释执行完成后,接着在根据 PC 寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
JIT, just in time compiler 即时编译器
JVM 将字节码直接编译成和本地机器平台相关的机器语言。主流 JVM 如 HotSpot 虚拟机采用解释器与JIT并存的结构。
早期的虚拟机是只采用解释器 interpreter 对字节码进行逐行解释执行,当虚拟机发现了热点代码时,会采用 JIT 编译的方式把字节码直接编译成机器码,加快执行速度。
- 时间开销:速度加快指的是编译后的执行速度加快,但编译需要一定的时间
- 空间开销:字节码编译成机器码后,内存膨胀10x。所以只对热点代码进行编译,否则会造成代码爆炸。
如何发现热点代码
两种方式:
- 周期性的对线程栈进行采样,出现频次高的。
- 缺点:线程由于阻塞或其他原因可能导致长期位于线程栈中,导致结果不准确
- 为方法/代码块建立计数器,次数达到阈值。
hot spot 虚拟机采用的是计数器法。
字节码主要是为了实现特定软件运行和软件环境、与硬件环境无关。