我们的java文件被编译成class文件,该class文件中就是包含字节指令
概述
虚拟机字节码执行引擎的工作模式
- 1.解释执行:通过解释器执行
- 2.编译执行:通过JIT即时编译器产生本地代码执行
运行时栈帧结构(基于线程的,在栈顶部的栈帧就是当前方法)
栈帧存储的数据
- 1.局部变量表
- 2.操作数栈(一个方法有一个栈)
- 3.动态链接
- 4.方法返回的地址
局部变量表大小和操作数栈的深度在编译程序代码的时候就确定
局部变量表
- 1.用于存放方法参数和方法内部定义的局部变量(即包含基本类型,引用和返回地址)
- 2.局部变量表的最小单位是slot,JVM只是规定slot能存放(基本类型,引用和返回地址),对于64位的类型可以使用2个slot
- 3.slot可以复用,只要过了占用slot的局部变量的作用域。当过了作用域,但是该slot没有被复用则数据不会被回收。因为slot属于GC Root
操作数栈(基于方法的)
- 1.jvm实现的时候线程的栈中下面栈帧的操作数栈会和上面的栈帧的局部变量表进行重合。
- 2.上面重合的目的是为了避免需要再开辟空间复制局部变量的数据,而是直接共用。
动态链接
- 1.每一个栈帧都包含一个指向运行时候常量池中该栈帧锁属方法的引用。
- 2.持有这个引用是为了支持方法调用过程中的动态链接。
方法返回地址
- 1.方法返回方式有两种:执行引擎遇到任意一个方法返回的字节码指令(正常退出),遇到异常但是该异常没有在方法体内得到处理(异常退出)
- 2.方法返回的时候需要一些信息(这些信息保存在栈帧中),才能返回到方法调用的位置并恢复上层调用方法。(异常退出时返回地址要通过异常处理器表来确定,但是栈帧不会保存这个)
- 3.信息包含:上层方法的局部变量表和操作数栈,返回值(把返回值(如果有的话)压入调用者的栈帧的操作数栈),PC计数器值(调整PC计数器值指向方法调用指令后面的一条指令)
方法调用
- 1.方法调用并不等于方法执行,方法调用主要就是确定调用方法的版本(比如是父类方法还是子类方法)
- 2.一切方法调用在class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。即只能是类加载期间甚至到运行期才能确定目标方法的直接引用。
解析
- 1.一部分方法的符号引用在类加载阶段解析的前提是这些方法在程序真正运行之前就有一个可确定的调用版本,且该版本不可变(静态方法和私有方法)。
- 2.invokestatic和invokespecial属于非虚方法,其他的都是虚方法。不包含final方法(虽然final方法也是采用invokevirtual来修饰的,但是不属于虚方法)
- 3.invokevirtual指令:知道操作数栈顶的第一个元素所指向的实际类型C,然后在类型C中找到与常量池中描述符合简单名称都相符合的方法,
然后进行访问权限校验,如果通过则返回直接引用。否则的话按照继承关系从下往上对C的各个父类进行之前的搜索和验证。
静态分派
- 1.Human human=new man(); Human属于静态类型,编译器就确定了。man属于实际类型。
- 2.对于方法重载的时候是根据静态类型来选择方法,但是如果方法内部使用了参数,则实际调用的是参数的实际类型的方法。
- 3.所以依赖静态类型来定位方法执行版本的分派动作我们成为静态分派,方法重载属于静态分派的一种类型。
动态分派
- 1.动态分派的典型应用是重写,具体可以参考invokespecial指令操作原理。
单分派和多分派
- 1.方法的接收者(方法执行的实际类型)与方法的参数都称为方法的宗量。宗量多就是多分派,宗量少就是单分派。当然了这个宗量必须对方法确定产生效果才算。
虚拟机动态分派的实现
- 1.由于动态分派会不停的寻找方法,会导致性能不好。
- 2.JVM为类在方法区建立一个虚方法表(即只给invokevirtual指令使用的,对于其他的指令也会建立相应的表)
- 3.虚方法表中存放着各个方法的实际入口地址。一个类A,如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类相同。
- 4.如果重写则子类的该方法的地址会替换成子类的实现版本的入口地址。
- 5.为了实现上的方便,具有相同签名的方法,在父类和子类的虚方法表中都应当具有一样的索引号,这样的好处就是当类型变换只需要替换查找的方法表。
- 6.除了方法表还有可能使用内联缓存和基于CHA的守护内联。
基于栈的字节码解释执行引擎
我们的程序代码物理是编译成虚拟机能执行的指令集还是物理机的目标代码大致逻辑如下
-
1.主逻辑:程序源码->词法分析->单词流->语法分析->抽象语法树
-
2.解释执行:抽象语法树->指令流->解释器->解释执行
-
3.编译执行:优化器->中间代码->生成器->目标代码
javac编译器完成了(独立于虚拟机之外):程序源码->词法分析->单词流->语法分析->抽象语法树->指令流
编译器输出的指令流基本上都是基于栈的指令集架构,当然还有依赖于寄存器的指令集。
- 1.基于栈的指令集的有点是可移植性强,代码紧凑,不需要考虑空间分配(都在栈上分配)。缺点是完成相同操作需要更多的指令(因为栈始终多进栈和出栈),因为栈是基于内存而内存的速度是很慢的(相对于处理器)。(虽然虚拟机可以采用栈顶缓存和常用的操作映射到寄存器中以避免直接内存访问,但是效果并不好)
- 2.寄存器指令集则是速度很快。缺点是可移植性强














网友评论