第二部分:运行时数据区
1.程序计数器:
全称是程序计数寄存器,像CPU的寄存器一样,存放线程的下一条指令的地址。每个线程都有一个
(区域小,执行速度快,不会有垃圾回收,也不会报oom错)
两个问题:
- 使用PC寄存器存储字节码指令地址有什么用呢 或者表述为 为什么使用PC寄存器记录当前线程的执行地址呢
- 因为CPU在不停地切换线程,当切换回来时,需要通过PC寄存器知道从哪个地方开始执行
- JVM的解释器也需要PC寄存器告诉它下一条指令在哪里
- PC寄存器为什么被设定为线程私有
- 因为CPU在不停地切换线程,当切换回来时,需要通过PC寄存器知道从哪个地方开始执行。要是只有一个PC寄存器,就不知道线程在中断时执行到哪个地方
2.虚拟机栈:
- 是线程私有的,线程创建时会创建一个虚拟机栈,栈里面有栈帧,对应一个个方法。
- 作用:管理Java方法的调用,保存方法的局部变量(不是成员变量)(8种基本类型,以及引用类型的引用),部分结果(中间结果),并参与方法的调用和返回
- 只会进行入栈出栈操作,不会GC,但是会OOM
- 速度快,仅次于程序计数器
- 栈越大,可调用方法越多。栈帧越大,可,越小。局部变量表和操做数栈越大,栈帧越大
会有OOM和 StackOverflow 异常,死循环 / 内存泄露 / 方法区class对象太多 会OOM ,一直自己调用自己或者递归可能SOF
栈帧:
- 栈顶的栈帧是当前栈帧,对应的是当前方法,当前方法所属的类是当前类
- 执行引擎只操作当前栈帧
- 如果当前方法调用了新的方法,会新建一个栈帧,放在栈顶,成为当前栈帧。当前方法返回,当前栈帧就会被抛弃,前一个栈帧成为当前栈帧
- 不同的线程有不同的栈,不能引用其他栈的栈帧
- Java方法有两种返回函数的方式:一是正常的return,void方法在字节码里有return指令,在源代码里面写不写return都行。二是有异常,但是没有处理,被抛出,也会退出程序,程序报错结束
- 内容:
- 局部变量表(Local Variables)(或 局部变量数组 本地变量表)
- 定义为一个数字数组(长度为32位以内的数据占一个slot,64位占两个slot)(其他数据类型都转为int,double,long占两个slot)
- slot是它的最基本的存储单元(变量槽)【引用变量只占一个槽】
- slot可以回收,当局部变量过了作用域,后面的局部变量可以重复利用它之前占的槽
- 主要用于储存方法参数和方法体里的局部变量,这些数据类型包括 基本数据类型,对象引用,returnaddress类型
- 局部变量表是建立在线程的栈上,是线程的私有数据,所以线程安全**(所以多线程使用同一个类的同一个方法,是线程安全的)**
- 表里面的变量只在当前方法调用中有效。方法调用完,栈帧销毁,表销毁
- 局部变量表的大小在编译期确定下来的。并保存在方法的code属性的maximum local varibles数据项中。方法运行期间是不会改变它的大小的。
- 操作数栈(Openrand Stack)(或称为 表达式栈)
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的储存空间
- 用数组实现
- 操作数栈是JVM执行引擎的一个工作区。当方法开始执行时,创建新的栈帧,栈帧里创建空的操作数栈
- 操作数栈的大小在编译期就已经确定,保存在方法的code属性中的max_stack
- 操作数栈中的元素可以是Java数据类型的任意一个。32位的类型占用一个深度。64位占用两个
- 操作数栈不是采用访问索引的方式进行访问。只能是通过入栈出栈进行操作
- 如果被调用的方法有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器指向下一条指令
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
- Java虚拟机的执行引擎是基于栈的执行引擎,里面的栈是操作数栈。
- 栈顶缓存技术:因为操作数栈是在内存中的,字节码指令多,读写操作频繁,所以hotSpot jvm的设计者把栈顶元素缓存到物理的寄存器中,提高速度
- 动态链接(Dynamic Linking)(或 指向运行时常量池的方法引用)
- 就是一个指向运行时常量池的方法引用
- 每个栈帧都有一个动态链接
- 作用:
- 使当前方法的代码支持动态链接。比如:invokedynamic指令
- 字节码文件里有一个常量池,里面是变量和方法的引用,都是符号引用。【动态链接的作用就是把符号引用转换成调用方法的直接引用】
- 常量池:作用就是为了提供一些符号和常量,便于指令的识别
- 方法返回地址(Return Address)(或 方法正常退出或异常退出的定义)
- 作用:存放调用该方法的方法的PC寄存器的值,
- 方法正常退出:调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址
- (不是很清晰)有未捕获异常退出:直接退出该方法,会给该方法的调用者产生返回值
- 如果方法中有try catch异常,捕获异常时,根据异常表,正常捕获跳转
- 共同点:调用结束之后,都返回调用该方法的位置
- 返回指令:ireturn(返回值为boolean,byte,char,short,int)、lreturn、freturn、dreturn、areturn(返回引用)、return(void方法,实例初始化方法,类和接口的初始化方法)
- 作用:存放调用该方法的方法的PC寄存器的值,
- 一些附加信息,例如:对重新调试提供支持的信息
- 【有时会把 动态链接, 方法返回地址, 一些附加信息统称为 帧数据区】
- 方法的调用:
- 链接与绑定
- 静态链接与动态链接
- 都是把符号引用换成直接引用。区别是前者需要被调用方法在编译期可知,且运行时不变。后者是被调用方法在编译器无法确定下来,只能在运行时转换
- 早期绑定和晚期绑定
- 绑定是一个字段,方法,类在符号引用转换成直接引用的过程,这仅仅发生一次
- 都是将方法和所属类型绑定。区别是前者是方法在编译期确定,且运行期不发生改变,后者是方法在编译期不确定
- 面向对象的编程语言都有多态,自然具备早期绑定和晚期绑定两种方式
- 静态链接与动态链接
- 虚方法
- Java中的普通方法都具有虚方法的特征,相当于C++的虚函数。如果希望某个方法不具有虚函数的特征,加final修饰
- 非虚方法:方法在编译期间就确定了具体的版本,且运行时不变
- 静态方法,私有方法,父方法,final方法,构造方法都是非虚方法
- 其他方法为虚方法
- 虚拟机提供了几种方法调用指令:
- 普通调用指令
- invokestatic:调用static方法,编译期间确定唯一方法版本
- invokespecial:调用方法,父类方法,私有方法,编译期间确定唯一方法版本
- invokevirtual:调用虚方法
- invokeinterface:调用接口方法
- 动态调用指令:
- invokedynamic:动态解析需要调用的方法,然后执行
- Java7修改了虚拟机规范,在指令集加了这个指令。但是没有直接生成这个指令的方式。Java8的lambal表达式出来之后,才有了直接的生成方式
- invokedynamic:动态解析需要调用的方法,然后执行
- 前面四个指令不能干预,最后一个用户可以人为确定方法版本。 invokestatic+invokespecial+final方法(被分到invokevirtual里了)为非虚方法
- 普通调用指令
- Java中的普通方法都具有虚方法的特征,相当于C++的虚函数。如果希望某个方法不具有虚函数的特征,加final修饰
- 方法的重写
- 方法重写的本质:
- 1.找到操作数栈栈顶的第一个元素所执行的对象的实际类型,记作C
- 2.如果在类型C中找到和常量中的描述和简单名称都相符的方法,则进行访问权限校验。如果校验通过则返回这个方法的直接引用,查找过程结束。校验不通过,抛出 java.lang.IllegalAccessErrorr 异常
- 3.如果没有找到相符的方法,按继承关系从下往上对C的各个父类进行第二步的查找和校验过程
- 4.如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常
- 【IllegalAccessErrorr 介绍:当程序试图访问或修改一个属性或调用一个方法,如果你没有权限访问,就会引起编译器异常。如果这个错误发生在运行时,就说明这个类发生了不兼容的变化】
- 方法重写的本质:
- 虚方法表
- 作用:减少调用虚方法时查找目标方法的次数,用索引表代替查找【非虚方法不用查找,因为编译期就确定了】
- 每个类都有一个,表中存放着各个方法的实际入口
- 创建时间:虚方法表在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完后,JVM会把该类的方法表也初始化完毕
- 链接与绑定
- 局部变量表(Local Variables)(或 局部变量数组 本地变量表)
面试题:
- 举例栈溢出的情况:一直自己调用自己 或者 一直递归
- 调整栈的大小,能保证不出现溢出吗:不能。如果一直递归不退出,只能推迟出现异常的时间
- 垃圾回收是否会涉及到虚拟机栈:不会,栈会有OOM,但是没有GC
- 分配的栈内存越大越好吗:不是。要合理分配,过大会导致线程数过少,而且也会影响jvm其他部分使用的空间
- 方法中定义的局部变量是否线程安全:如果没有逃逸,只在方法内起作用,是线程安全的。但是如果逃逸了,比如传参进来,return出去,不仅仅在方法内起作用,就不是线程安全
3.本地方法栈
本地方法接口
- 本地方法:一个Java调用非Java代码的接口,方法体是非Java方法在外面实现的
- 使用本地方法的原因:
- 与Java环境外交互
- 与操作系统交互
- Sun’s java:sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互
- 现在使用本地方法越来越少了,除法是和硬件相关的应用,比如通过Java程序驱动打印机或Java系统管理生产设备。因为现在的异构邻域间的通信很发达,比如用socket通信,用web service
- native可以与除了 abstract 的所有修饰符一起用
本地方法栈
- 也是线程私有的
- 允许被实现成固定或动态大小(溢出和 虚拟机栈是一样的)
- 本地方法是用C写的
- 具体做法是在本地方法栈 登记本地方法,执行引擎执行时加载本地方法库
- 当线程调用一个本地方法时,它就不受虚拟机限制,和虚拟机有一样的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 它可以直接使用本机的寄存器
- 直接从本地内存的堆中分配任意数量的内存
- 不是所有虚拟机支持本地方法
- 在hotspot jvm中,直接将本地方法栈和虚拟机栈合二为一