基于<<深入理解Java虚拟机>>做的笔记
文章目录
概述
物理机的执行引擎是直接建立在处理器,缓存,指令集的操作系统层面上的
虚拟机执行引擎由软件实现,可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行不受硬件直接支持的指令集格式
执行引擎在执行字节码时,有解释执行,编译执行两种选择。但是输入输出都是一致的:输入字节码二进制流,处理过程是字节码解析执行的等效过程,输出是执行结果
运行时栈帧结构
虚拟机以方法为最基本的执行单元,栈帧对应一个方法,是虚拟机运行时数据区的虚拟机栈的栈元素。每一个方法从调用开始到执行结束的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程
栈帧存储了局部变量表,操作数栈,动态连接,方法返回地址和附加信息
一个栈帧需要分配多少内存,在编译程序源码时就被计算出来并且写入到方法表的Code属性中了,并不会受到程序运行期变量数据的影响,仅仅取决于程序源码和具体的虚拟机实现的栈内存布局
在Java程序的角度看,同一时刻,同一线程上,在调用堆栈的所有方法都同时处在执行状态
而对于执行引擎,在活动线程中,只有位于栈顶的方法是运行的,被称为当前栈帧和当前方法
局部变量表
是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量
局部变量表以变量槽位最小单位,一个变量槽可以存放一个32位以内的数据类型
引用至少要做到两点:
1. 根据引用直接或间接地查找对象在Java堆中的数据存放的起始地址或索引
2. 根据引用直接或间接地查找对象所属数据类型在方法区中的存储的类型信息
对于64位数据,会分配两个连续的变量槽空间(long,double),对他们读写分割位两次32位读写。由于局部变量表是建立在线程堆栈中的,属于线程私有的数据,所以无论两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题
当方法被调用,虚拟机会用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果是实例方法,则局部变量表第0位索引的变量槽默认存放用于传递方法所属对象实例的引用,即this
变量槽可重用,出作用域的可以重新分配
局部变量表没有准备阶段,所以如果一个局部变量定义了但是没有赋初始值,则不能使用,编译器在编译期间就能检查到并提示出这一点
操作数栈
最大深度也在编译时就写入了Code属性的max_stacks里
32位数据类型所占的栈容量位1,64所占为2,任何时候操作数栈的深度都不会超过max_stacks设置的最大值
做算术运算时,通过将运算涉及的操作数压入栈顶后调用运算指令来进行的
如iadd指令,运行时要求操作数栈中栈顶和次顶元素已经存了两个int型,执行该指令会将两个int出栈并相加,然后重新入栈
动态连接
为了支持方法调用过程中的动态连接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
方法返回地址
两种方式退出方法:
正常调用完成:执行引擎遇到任意一个方法返回的字节码指令,这时可能有返回值传递给上层的方法调用者
异常调用完成:方法执行过程遇到异常,且该方法的异常表中没有搜索到匹配的异常处理器
在方法退出之后,都必须返回到最初方法被调用时的位置。如果是正常退出,则保存了主调方法的PC计数器的值
方法退出时:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令
附加信息
可增加一些规范中没有描述的信息到栈帧之中,例如与调试,性能收集相关的信息
一般把动态连接,方法返回地址,附加信息归为一类,称为栈帧信息
方法调用
确定被调用方法的版本(即调用哪一个方法),暂时未涉及方法内部的具体运行过程
一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(即直接引用)
所以某些调用需要在类加载期间,甚至在运行期间才能确定目标方法的直接引用
解析
在类的解析阶段,会将其中的一部分符号引用转为直接引用,前提是这些方法在程序真正运行之前就有一个可确定的调用版本,且在运行期不改变
符合"编译期可知,运行期不可变"的,主要有静态方法和私有文件两大类.前者与类型直接关联,后者在外部不可被访问
调用字节码指令
- invokestatic 用于调用静态方法
- invokespecial 用于调用()方法,私有方法,父类的方法
- invokevirtual 用于调用所有的虚方法
- invokeinterface 用于调用接口方法,会在运行期再确定一个实现该接口的对象
- invokedynamic 先在运行期动态解析出调用点限定符所引用的方法,然后再执行
只要能被invokestatic,invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本
符合这些条件的方法共有:静态方法,私有方法,实例构造器,父类方法,再加上被final修饰的方法(尽管他被invokevirtual 修饰)
这五类方法再类加载时就可以把符号引用解析为该方法的直接引用,统称为非虚方法
解析调用一定是静态的过程
分派
静态分派
static abstract class Human
{
}
static class Man extends Human
{
}
static class Woman extends Human
{
}
public void sayHello(Human guy)
{
System.out.println("Human");
}
public void sayHello(Man guy)
{
System.out.println("Man");
}
public void sayHello(Woman guy)
{
System.out.println("Women");
}
public static void main(String[] args)
{
Human man = new Man();
Human women = new Woman();
MainTest mainTest = new MainTest();
mainTest.sayHello(man);
mainTest.sayHello(women);
/*
result:
Human
Human
*/
}
Human man = new Man();
中,Human称为静态类型,Man称为实际类型
最终的静态类型是再编译期可知的,实际类型变化的结果在运行期才可确定
虚拟机在重载时是通过参数的静态类型而不是实际类型作为判断依据的。在编译阶段,编译器根据参数的静态类型决定使用哪个重载版本
所有依赖静态类型来决定方法执行的版本的分派动作,都称为静态分派,最典型的应用就是方法重载
编译器虽然能确定出方法的重载版本,但是很多情况下重载版本并不唯一,往往只能确定一个相对更适合的版本
动态分派
是多态性中的重写的变现。
在运行期根据变量的实际类型来分派方法执行版本
字段永远不参与多态
如今的Java语言是一门静态多分派,动态单分派的语言
invokevirtual解析过程
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束。不通过则返回IllegalAccessError异常
- 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
- 如果始终找不到合适的方法,则抛出AbstractMethodError
虚拟机动态分派的实现
常见的优化手段是在方法区建立一个虚方法表(vtable),使用虚方法表索引来代替元数据提高查找性能
虚方法表中存放着各个方法的实际入口地址,如果某个方法没有在子类中被重写那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。
如果子类重写了,则替换为指向子类实现版本的入口地址。
虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕
不使用final修饰的默认都是虚方法
动态类型语言支持
invokedynamic指令,为实现动态类型语言支持而产生
动态类型语言
关键特征是:它的类型检查的主体过程是在运行期而不是编译期进行的
运行时异常:只要代码不执行到这一行就不会产生异常
连接时异常:即使代码放在一条根本无法被执行到的路径分支上,类加载时也照样会抛出异常
动态类型语言另外一个核心特征:变量无类型,变量值有类型
优缺点
- 静态类型语言能够在编译期确定变量类型,编译器可以提高全面严谨的类型检查,利于稳定性,让项目更容易达到更大规模
- 动态类型语言运行期才确定类型,为开发人员提供极大的灵活性,更清晰明了,意味着开发效率的提高
java.lang.invoke包
MethodHandle与Reflection的区别
- Reflection是在Java代码层次模拟的方法调用,MethodHandle是模拟字节码层次的方法调用
- Reflection是Java端的全面映像,是重量级的,MethodHandle是轻量级的
- Reflection难以进行优化,MethodHandle可以实现各种优化(如方法内联等)
- Reflection只是为Java语言服务,MethodHandle则设计为可服务于所有Java虚拟机语言
基于栈的字节码解释执行引擎
探讨如何执行方法里面的字节码指令.有解释执行,编译执行两种
解释执行
在执行前,先对程序源码进行词法分析和语法分析处理,把源码转换为抽象目录树。
基于栈的指令集和基于寄存器的指令集
Javac编译器输出的字节码指令流,基本上是一种基于栈的指令集架构。字节码指令流中大部分是零地址指令,依赖操作数栈进行工作。
例如1+1:
iconst_1
iconst_1
iadd
istore_0
两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈,相加,然后把结果放回栈顶,最后istore_0把栈顶的值放到局部变量表的第0个变量槽中
优缺点
- 基于栈的主要优点是可移植,用栈架构,用户程序不会直接使用寄存器,可以由虚拟机实现来将一些访问最频繁的数据(程序计数器,栈顶缓存等)放到寄存器中以提高性能。代码紧凑,编译器实现简单
iconst_1
iconst_1
iadd
istore_0
两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈,相加,然后把结果放回栈顶,最后istore_0把栈顶的值放到局部变量表的第0个变量槽中
优缺点
- 基于栈的主要优点是可移植,用栈架构,用户程序不会直接使用寄存器,可以由虚拟机实现来将一些访问最频繁的数据(程序计数器,栈顶缓存等)放到寄存器中以提高性能。代码紧凑,编译器实现简单
- 缺点是执行速度稍慢,完成相同功能所需的指令数量要多,频繁的栈访问也就意味着频繁的内存访问