JVM---运行时数据区-Java虚拟机栈
Java虚拟机栈
/** * 【运行时数据区-Java虚拟机栈】 * * JVM指令是 基于栈的指令架构,基于寄存器的指令架构依赖具体的平台,所以基于栈的指令架构可以跨平台; * 栈是运行时的单位,堆是存储的单位; * * Java虚拟机栈Java Virtual Machine Stack: * * what? * 每个 线程 在创建时 都会创建一个 Java虚拟机栈, Java虚拟机栈 内 保存 一个个的栈桢(对应着 一次次的方法调用); * 线程私有; * 声明周期同当前线程; * * 作用: * 主管Java程序的运行; * 保存 方法的 局部变量、临时计算结果,并 参与 方法的调用 和 返回; * * 栈的特点: * 快速有效的分配存储方式,访问速度仅次于PC寄存器; * JVM对 Java虚拟机栈的操作:入栈、出栈; * Java虚拟机栈 不存在 GC问题; * * Java虚拟机栈可能存在的异常: * ***Java虚拟机 允许 Java虚拟机栈 的大小 是 动态的 或者 是 固定的; * * a,如果采用 固定大小, 每个线程的 Java虚拟机栈 容量 在创建的时候选定; * 如果 线程 请求的栈容量 超过 Java虚拟机栈 允许的最大容量,JVM 将抛出 StackOverflowError; * b,如果采用 动态扩展, * Java虚拟机栈 在尝试扩展时 无法 申请到 足够的内存 (或者 线程 创建 没有足够内存 去创建 Java虚拟机栈时)时,JVM将会 抛出 OutofMemoryError; * * Java虚拟机栈大小设置: * -Xss * 默认以byte为单位,后面加单位进行单位变更; * eg: 1KB, 1MB, 1GB... * * Java虚拟机栈的存储结构: * 栈中 的 数据 以 栈桢Stack Frame 的格式存在; * 每个方法 对应 一个 栈桢; * JVM对Java虚拟机栈的操作只有2个,就是对栈桢的 压栈push、出栈pop,遵循 先进后出的 原则; * 在一条活动线程中,一个时间点上,只有一个活动的栈桢,即只有当前正在执行的方法的栈桢是有效的,被称为 当前栈桢; * 执行引擎运行的所有字节码指令 只对 当前栈桢进行操作; * 如果该方法调用了其他方法,其他方法的新栈桢会被创建出来,放在栈的顶端,称为新的当前栈桢; * * ****不同线程中的栈桢 是 不允许 存在相互引用的,不可能在一个栈桢中引用另外一个线程的栈桢; * * Java方法的2种返回方式(2种方式 都会将 当前栈桢 弹出): * a,return指令 * b,抛出异常 * */
栈桢-局部变量表
/** * 【Java虚拟机栈-栈桢-局部变量表】 * 1、局部变量表local variables * 局部变量表 本质是一个 一维数字数组; * * 作用: * 存储 方法参数、定义在方法内的局部变量(包括 基本数据类型、引用数据类型、returnAddress类型)的值; * * 内存大小: * 局部变量表所需内存大小 在 编译期 就确定,并 保存在 方法的Code属性的 maximum local variables数据项中(运行期 不会 改变); * * eg : * * 12 public void test(int a){ * 13 Date date = null; * 14 String s = null; * 15 int aa = 0; * 16 } * * bytecode: * 0 aconst_null * 1 astore_2 * 2 aconst_null * 3 astore_3 * 4 iconst_0 * 5 istore 4 * 7 return * * LineNumberTable: * 字节码指令 与 Java源代码 的映射关系; * * Nr 起始PC 行号 * 0 0 13 * 1 2 14 * 2 4 15 * 3 7 16 * * LocalVariablesTable: * 方法内的局部变量的描述; * * Nr 起始PC 长度 序号 名字 描述符 * 0 0 8 0 cp_info #9 cp_info #10 * this * 1 0 8 1 cp_info #13 cp_info #14 * a * 2 2 6 2 cp_info #15 cp_info #16 * date * 3 4 4 3 cp_info #17 cp_info #18 * s * 4 7 1 4 cp_info #19 cp_info #14 * aa * * * Slot(槽): * 局部变量表,最基本的存储单元是 Slot; * * 局部变量表中,32位以内的类型 只占 一个Slot,64位的类型 占2个Slot; * byte, short, char 在存储前 被 转换为 int, * boolean 也被转换为 int, * 0 表示 false,1 表示 true; * * ***long、double 占 2个Slot,引用类型、byte、short、int、char、boolean占 1个slot; * * JVM 为 局部变量表的每个Slot 分配一个访问索引,通过这个索引 可以访问 局部变量表指定的 局部变量值; * 如果需要访问 局部变量表的64bit的局部变量值时,只需要前一个索引即可; * * 当一个方法 被调用时,它的 方法参数、局部变量 被 按照声明顺序 复制到 局部变量表的每个Slot中; * * 如果当前栈桢 由 构造方法(或 非static方法)创建,该对象引用的this将会 放在 index为0的Slot处,其余 按声明顺序排列; * ***为什么static方法中不能使用非static? * static方法中的局部变量表中没有this; * * 局部变量表的Slot 是可以重复利用的; * * 局部变量表的 变量 也是 GC的 根节点; * 只要被 局部变量 直接(或 间接)引用的 对象 不会被回收; */
栈桢-操作数栈
用途
用于方法的字节码指令执行时 存放操作数和临时结果;
/** * 【Java虚拟机栈-栈桢-操作数栈】 * 2、操作数栈operand stack * * 本质是一个 一维数字数组; * * eg : * public void test(int a){ * * byte b = 15; * int c = 8; * * int d = b + c; * } * * * PC寄存器: * 0-2-3-5-6-7-8-9-11 * * 0 bipush 15 将 15 push到 操作数栈 * 2 istore_2 将 操作数栈 的15 pop,存储到 局部变量表的 2序号slot * 3 bipush 8 将 8 push到 操作数栈 * 5 istore_3 将 操作数栈 的8 pop,存储到 局部变量表的 3序号slot * 6 iload_2 从 局部变量表的2序号slot 取出值15,push到 操作数栈 * 7 iload_3 从 局部变量表的3序号slot 取出值8,push到 操作数栈 * 8 iadd 将 操作数栈的8pop,将 操作数栈的15pop,然后 由执行引擎 将字节码指令翻译为本地机器指令, 然后 由CPU 进行相加运算得结果23,将23push到操作数栈; * 9 istore 4 将 操作数栈中的23pop,将23 存储到 局部变量表的4序号slot * 11 return * * LocalVariableTable * 序号 * 0 0 12 0 cp_info #9 cp_info #10 * this * 1 0 12 1 cp_info #13 cp_info #14 * a * 2 3 9 2 cp_info #15 cp_info #16 * b * 3 6 6 3 cp_info #17 cp_info #14 * c * 4 11 1 4 cp_info #18 cp_info #14 * d * * 作用: * 在 方法执行过程中,根据字节码指令,往 操作数栈 中 写入push (提取pop) 数据; * * 内存大小: * 在 编译期 确定,保存在 方法的Code属性的 max_stack ; * * 根据 字节码指令 将 操作数 写入 操作数栈 (或 将 操作数 从 操作数栈 取出),CPU计算完成后,将 结果 再写入 操作数栈; * 如果 方法有返回值,其返回值 将被 写入 当前栈桢 的 操作数栈中,并 更新 PC寄存器中 下一条要执行的字节码指令; * JVM的 解释引擎 是 基于 栈 的执行引擎,其中 栈 指的就是 操作数栈; * * ***byte, short, char, boolean 在 进行 运算时 ,都会转化为 int 类型 进行计算; * * 栈顶缓存技术: * HotSpot JVM设计者,为了 解决 频繁 执行 内存 读/写,将 栈顶元素 全部 缓存 在物理CPU的寄存器中; */
栈桢-动态链接
what
程序执行过程中, 将 方法调用的符号引用 转换为 直接引用的过程;
通过 方法区运行时常量池 来实现;
符号引用&直接引用
符号引用:
是在编译时生成的类文件中的信息,它包含了方法和字段的名字、描述符以及它们所在的类或接口的全限定名。
这些信息被存储在类文件的常量池中。
直接引用:
是程序运行期间可以直接定位到目标方法或字段的具体地址。
这个地址可能是一个指针、一个偏移量或者是一个句柄等具体的数据结构。
方法调用过程
当一个方法被调用时,JVM会首先查找该方法的符号引用,并将其转化为直接引用。这个过程通常涉及到以下步骤:
-
解析:JVM检查当前栈帧中的方法引用是否已经被解析过。如果没有,JVM则会开始解析过程。
-
绑定:解析过程实际上就是进行绑定,即将符号引用与具体的内存地址关联起来。这一步骤可以在类加载时完成(静态绑定),也可以在运行时完成(动态绑定)。对于虚方法(非
final
和非private
的方法),动态绑定会在每次方法调用时进行。 -
访问权限检查:在将符号引用转化为直接引用的过程中,JVM还会进行访问权限检查,确保当前线程具有调用该方法的权限。
-
方法调用:一旦方法的直接引用确定,JVM就可以通过这个引用来调用方法了;
/** * 【Java虚拟机栈-栈桢-动态链接】 * 动态链接dynamic linking * what? * 当前Class 常量池 在 方法区常量池 的引用; * 作用: * 将 运行时常量池 的符号引用 转换为 直接引用; * * Java源代码 经过 编译到 字节码文件中时,所有的变量、方法引用 都作为 符号引用 保存在 class文件的常量池中; * * 每个 栈桢 内部 都包含 一个 指向 运行时常量池 该栈桢所属方法 的引用; * 引用 的目的: * 支持当前方法的代码 能够 实现 动态链接,比如 invokedynamic指令; * * ***方法调用*** * 在 JVM中,将 符号引用 转化为 直接引用 与 方法的绑定机制 相关; * * 绑定: * 一个字段(或 方法, 或 类)在 符号引用 被 转化为 直接引用 的过程,仅发生一次; * * 方法的绑定机制: * 静态链接(早期绑定): * 当一个 Class文件 被 加载到 JVM内存时,如果 被调用的 目标方法 编译期 可知 , 且 运行期 不变; * 这种 情况下 将 调用方法的 符号引用 转化为 直接引用 的过程 称为 静态链接; * eg: 非虚方法 * * 动态链接(晚期绑定): * 当一个 Class文件 被 加载到 JVM内存时,如果 被调用的 目标方法 编译期 无法确定,只能在程序 运行期 将 符号引用 转化为 直接引用; * 这种 引用 转化过程 具备 动态性,称为 动态链接; * eg:虚方法 * * ***多态的实现*** * 多态的使用前提: * 1、继承关系 2、方法重写 * * * ***虚方法与非虚方法*** * Java中的任何一个 普通方法 都具备虚方法的特征,如果不希望某个方法拥有虚方法的特征时,可以使用final修饰; * * 非虚方法: * 如果 方法 在编译期 就确定了 具体的版本 , 且 运行期 不可变,这样的方法就是 非虚方法; * eg:static方法、private方法、final方法、构造器、子类调用指定的父类方法; * * invokestatic:调用 static方法; * invokespecial:调用 构造器、private方法、父类方法; * invokevirtual:调用 final方法; * * 虚方法: * 方法 在编译期 无法确定 具体的版本; * * invokevirtual:调用 虚方法; * invokeinterface:调用 接口方法; * * 动态调用指令: * invokedynamic * Java7 增加了一个 invokedynamic指令,使得Java增加了动态语言类型支持; * * 动态解析出需要调用的方法,然后执行; * invokestatic、invokespecial、invokevirtual、invokeinterface 方法的调用执行不可人为干预,而invokedynamic则支持用户确定方法版本; * * ***动态类型语言与静态类型语言*** * 对类型的检查 在编译期,就是静态类型语言; * 判断变量自身的类型信息; * * 对类型的检查 在运行期,就是动态类型语言; * 判断变量值的类型信息; * * 区别: * 对 类型的检查 是在 编译期 还是 运行期; * * eg: * interface Fun{ * boolean exec(String s); * } * * public class DLTest { * * public void exec(Fun fun){} * * public static void main(String[] args) { * * DLTest dlTest = new DLTest(); * * dlTest.exec(s -> false); * * } * * 0 new #2 <com/an/stack/dynamiclinking/DLTest> * 3 dup * 4 invokespecial #3 <com/an/stack/dynamiclinking/DLTest.<init>> * 7 astore_1 * 8 aload_1 * 9 invokedynamic #4 <exec, BootstrapMethods #0> * 14 invokevirtual #5 <com/an/stack/dynamiclinking/DLTest.exec> * 17 return * * } * * * ***方法重写*** * 方法重写的本质: * 1、找到 操作数栈 的元素的对象的实际类型C; * 2、如果 在类型C中找到与常量池中描述符相符的方法,则进行访问权限校验,如果通过,则返回这个方法的直接引用,查找过程结束; * 如果不通过,则返回 java.lang.IllegalAccessError; * * 如果 在类型C中找不到,则根据继承关系,依次对C的各个父类进行搜索,如果父类中找不到,抛出java.lang.AbstractMethodError; * * ***虚方法表*** * 在面向对象编程中,很频繁使用 动态链接,如果每次动态链接都要重新查找,严重影响效率; * 因此,为了提升性能,JVM在类的方法区建立一个虚方法表virtual method table(非虚方法不出现在表中)来实现; * * 每个类都有一个虚方法表,表中放着各个虚方法的实际入口; * * * */
栈桢-方法返回地址
/** * 【Java虚拟机栈-栈桢-方法返回地址】 * 方法返回地址return address * 作用: * 存放 调用当前方法 PC寄存器的值; * * eg: * 方法A 调用 方法B * 当前栈桢B 正常执行完成后,将 方法A调用方法B的指令地址 交由 执行引擎; * * 方法的结束: * a,正常执行完成 * b,出现未处理异常,非正常退出 * * 无论 方法通过 哪种方式退出, 在方法退出后 都返回 该方法被调用的位置; * a,正常退出时,调用当前方法的 PC寄存器的值 作为返回地址; * b,异常退出时,返回地址 通过 异常表 来确定,栈桢不会保存 这部分信息; * * ***正常退出 与 异常退出 的区别*** * 通过 异常退出的方法 不会 给 调用者 返回任何值; * * * 正常退出: * 执行引擎 遇到任意一个方法返回的字节码指令,会有返回值传递给上层方法调用者,称为 正常完成出口; * * 一个方法 在 正常调用完成后 需要使用哪个返回指令 需要根据 方法返回值 的实际数据类型 确定; * ireturn: * 返回值类型是 byte, short, int, char, boolean * lreturn: * 返回值类型是 long * freturn: * 返回值类型是 float * dreturn: * 返回值类型是 double * areturn: * 返回值类型是 引用类型 * return: * void方法、构造器 * * 异常退出: * 在方法执行的过程中遇到了一次,且这个异常没有在方法内处理(在本方法的异常表中没有找到匹配的异常处理器),就会导致方法退出,称为 异常完成出口; * * 方法执行过程中抛出异常时的异常处理,存储在一个异常表; * * eg: * public void test1() { * try { * test2(); * } catch (ClassNotFoundException e) { * e.printStackTrace(); * } * } * * 0: aload_0 * 1: invokevirtual #2 // Method test2:()Ljava/lang/String; * 4: pop * 5: goto 13 * 8: astore_1 * 9: aload_1 * 10: invokevirtual #4 // Method java/lang/ClassNotFoundException.printStackTrace:()V * 13: return * * Exception table: * from to target type * 0 4 7 Class java/lang/ClassNotFoundException * * * */