概述
执行引擎是JVM核心的组成部分之一
虚拟机与物理机的区别
- 物理机
执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的 - 虚拟机
执行引擎由软件自行实现的,因此可以不受物理条件制约指令集和执行引擎的结构体系,能够执行不被硬件直接支持的指令集格式
不同的VM实现,执行引擎再执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)2中选择
所有的JVM执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果
运行时栈帧结构
JVM以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持JVM进行方法调用和方法执行背后的数据结构
每一个方法从调用开始至执行结束的过程都对应着一个栈帧在JVM栈里面从入栈到出栈的过程
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息
在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多多深的操作数栈就已经被分析计算出来,并写入到方法表的Code属性之中
子活动线程中,只有位置栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Currenr Stack Frame),与这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所允许的所有字节码指令都只针对当前栈帧进行操作
局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
为了尽可能节省栈帧好用的内存空间,局部变量表中的变量槽是可以重用的,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的变量槽就可以交给其他变量来重用
- 在某些情况下,变量槽的复用会直接影响到系统的垃圾收集行为
/**
* @author jtao
* @Date 2021/1/28 11:33
* @Description 局部变量表槽复用对垃圾收集的影响
* VM参数:-verbose:gc
*/
public class TestLocalVariablesTables {
@Test
public void testGC() {
{
byte[] placehoder = new byte[64 * 1024 * 1024];
System.gc();
}
}
@Test
public void testGC1() {
{
byte[] placehoder = new byte[64 * 1024 * 1024];
}
System.gc();
}
@Test
public void testGC2() {
{
byte[] placehoder = new byte[64 * 1024 * 1024];
}
int a=0;
System.gc();
}
}
对象能否被回收的根本
对上述TestGC、TestGC1、TestGC2方法分别运行得到的结果
内存没有被回收,因为变量placeholder还处于作用域之内
placeholder被限制在代码块之内,执行gc的时候已不能被访问,但还是没有被回收
只有TestGC2的内存被回收
变量能否被回收的根本原因在于:局部变量表中的变量槽是否还存有关于变量数据对象的引用。 前2种情况placeholder的变量槽没有改变,没有被其他变量所引用。所以无法回收,一般这种关联没有被及时打断影响都很轻微。
类的字段变量由2次赋值过程,一次是准备阶段,赋予系统初始值,另一次是初始话阶段,赋予程序员定义的初始值。因此类变量可以不赋值也有初始值,但成员变量不可以,编译无法通过
/**
* @author jtao
* @Date 2021/1/28 11:29
* @Description
*/
public class Test {
static int a;
public static void main(String[] args) {
int b;
System.out.println(a);
// System.out.println(b);
}
}
操作数栈(Operand Stack)
操作数栈也成为操作栈,先入后出栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作
在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是大多数JVM实现都会进行一些优化处理,令2个栈帧出现一部分重叠。让下面的栈帧的部分操作数栈与上面的栈帧的部分局部变量表重叠在一起。可以系欸月空间,且在方法调用时可以直接共用一部分数据,无需进行额外的参数复制传递
动态链接
每个栈帧都包含一个只想运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法。
- 正常调用完成
执行引擎遇到任意一个方法返回的字节码指令,如果有返回值则传递给上次的方法调用者 - 异常调用完成
执行过程遇到异常,且没有在方法体内得到妥善处理。只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。不会给上层调用者提供任何返回值
方法的推出的过程实际上等同于把当前栈帧出栈
附加信息
《Java虚拟机规范》允许JVM实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关信息
方法调用
该阶段的目的是确定被调用方法的版本(即调用哪一个方法),未涉及方法内部的具体运行过程
解析
方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。
调用目标在程序代码写好、编译器都进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution)
分派
静态分派
//TODO
动态分派
//TODO
单分派与多分派
//TODO
虚拟机动态分派的实现
//TODO
动态类型语言支持
JDK7的项目目标:实现动态类型语言,是为JDK8里可以顺利实现Lambda表达式而做的技术储备
动态类型语言
- 动态语言
关键特征为它的类型检查的主题过程是在运行期间而不是编译器执行的
如APL、Erlang、Groovy、PHP等 - 静态语言
在编译器就进行类型检查过程的语言
如C++、Java
一门语言的哪一种检查行为要在运行期进行哪一种检查要在编译期进行没有必然的逻辑关系,是由语言规范中认为设立的约定
动静态语言的权衡
- 静态语言
能够在编译器确定变量类型,最显著的好处是编译器可以提供全面严谨的类型检查,这样与数据类型相关的潜在问题就能在编码时被即使发现,利于稳定性及让项目容易达到更大的规模 - 动态语言
在运行期才确定类型,可以为开发人员提供极大的灵活性,某些在静态类型语言中要花大量臃肿代码来实现的功能,由动态类型语言去做会很清晰简洁
Java与动态类型
动态类型方法调用的底层问题再JVM层次上解决嘴和是
java.lang.invoke包
JDK7时薪加入的包
//TODO
invokedynamic指令
//TODO
实战:掌握方法分派规则
//TODO
Comments | 0 条评论