虚拟机栈
概述
- 栈是运行时的单元堆是存储的基本单位,栈是解决程序的运行问题, 即程序如何执行或者如何处理数据。 堆解决的是数据存储的问题。对象放在堆, 基本数据类型、引用对象的地址放在栈。
- 每一个线程在创建的时候都会创建一个栈,其内部保存一个个的栈帧,对应着一次次的java方法调用,是线程私有的
- 生命周期和线程一致
- 作用: 主管java程序的运行, 保存方法的局部变量(8种基本数据类, 对象的引用地址)、部分结果,并参与方法的调用和返回。
- 栈是一种快速有效的分配存储方式, 访问速度仅次于程序计数器
- 他的操作只有两个
- 进栈
- (方法执行完成)出栈
- 不存在垃圾回收问题
栈中可能出现的异常
- java虚拟机规范允许java栈的大小是动态的或者是固定不变的
-
设置为固定值 如果栈容量超过会造成StackOverflowError的错误
-
如果java栈是动态扩展的 如果没有足够的内存扩展了就会抛出OutOfMemoryError
-
栈的存储单元
- 栈中的数据是栈帧为基本单位的
- 这个线程上正在执行的每一个方法都各自对应一个栈帧
- 栈帧是一个内存区块, 是一个数据集,维系中方法执行过程中的各种数据信息。
- 活动的线程中, 在一个时间点上, 只会有一个会动的额栈帧, 即只有当前的正在执行的方法的栈帧是有效的, 称为当前栈帧, 与其对应的是当前方法
- 如果方法中调用了其他方法, 对应新的栈帧会被创建出来, 放在栈的顶端, 成为新的当前帧
- 运行原理
- 不同线程中所包含的栈帧是不允许存在相互引用的
- 如果当前方法调用了其他方法, 方法返回的时候就会栈帧就会传回结果, 丢弃当前栈帧使前一个栈帧重新成为当前栈帧
- java有两种返回函数的方式(都会导致栈帧弹出)
- 正常函数的返回
- 抛出异常
栈帧的内部结构
- 内部存着:
- 局部变量表
- 操作数栈
- 动态链接: 常量池的方法引用
- 方法返回地址: 正常退出或者异常退出的定义
- 一些附加信息
局部变量表
- 又称局部变量数组或者本地变量表
- 定义为一个数字数组, 主要用于存储方法参数和定义在方法体内的局部变量, 以及返回值的类型
- 是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定的, 运行期间不会改变其大小。编译的时候有一个locals指定大小
- 局部变量越大栈帧越大, 方法嵌套调用次数由栈的大小决定
- 局部变量表的变量只在当前方法调用中有效,在方法执行时, 虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程, 当方法调用结束, 销毁。
-
存储局部变量表的存储单元是Slot(变量槽)
-
32位以内的类型只占一个slot(包括返回值类型, 引用类型), 64占两个(long, double)
-
jvm会为每一个slot分配一个访问索引, 通过这个索引访问局部变量
-
当一个实例方法被调用的时候局部变量表的顺序按变量的声明顺序分配slot索引
-
都是用起始索引
-
对于构造器以及非静态方法会多一个索引, 该对象引用this将会存放在index为0的slot处(这就是为什么我们能调用this的原因以及非静态不能调this)
-
slt槽位可以重复利用, 出来作用域可以重复使用
-
这段代码对应的变量局部变量表
-
局部变量 一个start是局部变量的作用域 (在代码对应的就是变量声明的下一行开始)
-
变量的分类: 按照数据类型分类: ① 基本数据类型 ②引用数据类型
-
按照在类中的声明位置分为
- ① 成员变量 在使用前都会经历默认初始化赋值
* 分为 类变量(静态变量)在之前的链接中的准备给类变量默认赋值, 在初始化阶段显式赋值
* 实例变量: 随着对象的创建会在堆空间分配实例变量空间, 并进行默认赋值。 - ② 局部变量, 在使用前必须显式赋值。
- ① 成员变量 在使用前都会经历默认初始化赋值
-
局部变量表中的变量也是重要的垃圾回收根节点, 只要被局部变量表中直接或者间接引用的对象都不会回收。
操作数栈(数组实现的)
- 在方法执行的过程中, 根据字节码指令, 往栈中写入数据或者提取数据 例如: 执行赋值、交换、求和等。
- 主要用于暂时保存计算中间结果, 同时作为计算过程中变量临时的存储空间
- 刚开始操作数栈是空的
- 操作数栈同样也是在编译的时候确定的, stack
- 操作数栈并非是采用访问索引的方式来进行数据访问, 只能通过入栈和出栈来访问。
- 如果被调用的方法带有返回值的话, 其返回值会被压入当前栈帧的操作数栈中
- java虚拟机的解释引擎是基于栈的执行引擎 , 其中栈指的就是操作数栈
- 这段代码的在底层的变化
动态链接 (栈帧中的)
- 指向运行时常量池中该栈帧所属方法的引用,粗略就是指向常量池的指针
- 在java源文件被编译到字节码文件中时, 所有的变量和方法的引用都作为符号引用保存在class文件的常量池中。而动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
- 为什么需要常量池
- 节省空间, 更好的引用共享, 所以不能直接把直接引用放到字节码文件中, 那样会显得字节码文件很大。
- 而它的作用, 就是为了提供一些符号和常量, 便于指令的识别
方法调用
- 在jvm中, 将符号引用转化为调用方法的直接引用与方法的绑定机制相关。
- 静态链接(早期绑定)
当一个字节码文件装载进jvm内部时, 如果调用方法的目标方法在编译期可知, 且运行期保持不变, 这种情况下将调用方法的符号引用转换为直接引用的过程。 - 动态链接 (晚期绑定):在编译器无法确定, 在运行期才能确定
- 非虚方法
- 如果方法在编译器就确定调用版本, 并且在运行时不可变叫非虚方法
- 静态方法, private方法, final方法, 实例构造器, 父类方法
- 其他方法称为虚方法
- 子类对象的多态性的使用前提:①类的继承关系 ②方法的重写
- 虚拟机的方法调用指令
* - invokestatic 以及invokespecial指令调用的方法成为非虚方法, 其余final修饰除外都为虚方法
- invokedynamic 是动态类型语言java7才出来用于函数式编程(lamda表达式)
- java语言中方法重写的本质
- 找到操作数栈的第一个元素所执行的对象的实际类型, 记作C(调用一个方法会把它先压入栈中)。
- 如果在类型C中找到与常量中的简单名称都相符的方法, 进行访问权限的校验, 通过就返回这个方法的直接引用,结束,不通过, 返回java.lang.IllagalAccessError(无权限) (这个是先找自己的)
- 否则按照继承关系从下往上一次对C的个各个父类进行第二步。 (自己没有从下往上找父类或者接口的)
- 如果始终没有找到就会报java.lang.AbstactMethodError
- 会在每一个类的方法区生成虚方法表(非虚方法不会存在这里面)。(方便方法的直接调用不用在重复上述步骤)
方法返回地址
- 存放调用该方法的pc寄存器的值(记录上层调用者的pc寄存器)
- 无论通过(正常执行完还是异常退出), 在方法退出之后都要返回到该方法被调用的位置, 正常退出, 低佣者的pc寄存器的值会作为返回地址, 即调用该方法的指令的下一条指令的地址, 而通过异常退出的, 返回地址是要通过异常表来确定的, 栈帧不保存这部分信息
- 正常退出和异常退出的区别:通过异常完成退出的不会给上层调用者产生任何的返回值。
- 恢复上层调用则会的pc寄存器, 执行上层调用者的下一条指令
一些附加信息
- 可选的 不重要
面试题
- 举例栈溢出的情况? (StackOverflowError)
- 当栈帧放满整个栈的大小时就会溢出
- 同时可以通过 -Xss设置栈的大小
- 栈可以设置 固定大小以及可以扩容(如果出现扩容发现内存不足了就会出现OOM)
- 调整栈的大小, 就能保证不溢出么?
- 不能, 一直递归。 只能说让这个程序所拥有的栈空间更大, 而不能说肯定不会出现溢出
区域 | 是否会出现Error | Gc? |
---|---|---|
PC寄存器 | × | × |
虚拟机栈 | √ | × |
本地方法栈 | √ | × |
堆 | √ | √ |
方法区 | √ | √ |
- 分配的栈内存越大越好么?
- 不是自己用的越多其他人用的越少
- 垃圾回收是否会涉及虚拟机栈? 不会
- 方法中定义的局部是否线程安全 ?
* 是, 每个线程有单独的虚拟机栈, 只要是对于内部定义的局部变量是是安全, 如果return出去让其他人操作也有可能出现线程不安全的。
* 但如果是共享数据的话就是线程不安全的
什么是本地方法
- 一个nativ method就是一个Java调用非Java代码的接口
为什么要使用NativeMethod
- 与java环境外的交互
- 有时java应用需要与java外面的环境交互, 这是本地方法存在的主要原因。
- 与操作系统交互
- 通过使用本地方法, 我们得以用java实现了jre的与底层系统的交互, 甚至jvm的一些部分就是用C写的
- Sun的java
sun的解释器是用c实现的, 这使得它能像一些普通的c一样与外部交互
本地方法栈
- java虚拟机栈用于管理java方法的调用, 而本地方法栈用于管理本地方法的调用
- 线程私有的
- 当某个线程调用一个本地方法时, 它就进入了一个全新的并且不再受虚拟机限制的世界, 它和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来访问虚拟机的数据区。