基于栈而非特定寄存器。这种设计使得Java程序可以在不同的CPU架构上运行,避免了依赖特定硬件的限制。尽管这种做法带来了跨平台的优势,并且简化了编译器的实现,但也导致了性能上的一定下降。因为相比于基于寄存器的指令集,栈架构需要更多的指令来完成相同的任务。
栈是运行时的单位,堆是存储单位。
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack Frame),对应着一次次的Java调用。生命周期和线程一致,是线程私有的。主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。 -
JVM直接对栈的操作只有两个:每个方法执行,伴随着进栈(入栈、压栈);执行结束后的出栈工作。 -
对于栈来说不存在垃圾回收问题,即不存在GC,存在OOM。
栈中可能出现的异常
栈内存储的内容
栈的运行原理
-
JVM 对 Java 栈的操作仅限于栈帧的压栈和出栈,遵循先进后出(LIFO)的原则。 -
在单个活动线程中,任何时刻只有一个活动的栈帧,即当前正在执行的方法的栈帧(栈顶栈帧),称为当前栈帧(Current Frame),对应的方法为当前方法(Current Method),定义该方法的类为当前类(Current class)。 -
执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。 -
当方法调用其他方法时,会创建新的栈帧并置于栈顶,成为新的当前栈帧。 -
不同线程中的栈帧不允许相互引用,即一个栈帧不可能引用另一个线程的栈帧。 -
当前方法调用其他方法后,方法返回时,当前栈帧将传递执行结果给前一个栈帧,并丢弃当前栈帧,使前一个栈帧重新成为当前栈帧。 -
Java 方法有两种返回方式:正常返回(使用 return 指令)和异常抛出。无论采用哪种方式,都会导致栈帧被弹出。
-
也称为局部变量数组或本地变量表。 -
定义为一个数字数组,主要用于存储方法参数和方法体内定义的局部变量,包括基本数据类型、对象引用和returnAddress类型。 -
由于局部变量表建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题。 -
局部变量表的容量大小在编译期确定,并保存在方法的 Code 属性的 maximum local variables 数据项中,在方法运行期间不会改变。 -
方法嵌套调用次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。函数参数和局部变量越多,局部变量表膨胀,栈帧越大,方法调用占用的栈空间增加,导致嵌套调用次数减少。 -
局部变量表中的变量仅在当前调用方法中有效。虚拟机通过局部变量表完成参数值到参数变量列表的传递过程。方法调用结束后,随着方法栈帧的销毁,局部变量表也随之销毁。
在栈帧中,局部变量表中的槽位是可重用的。当一个局部变量超出其作用域后,后续声明的新局部变量很可能会复用已经失效的槽位,以节省资源。
// 以下代码是错误的,没有赋值不能够使用
public void test() {
int i;
System.out.println(i);
}
局部变量表中的变量也是重要的垃圾回收根节点,任何被局部变量表直接或间接引用的对象都不会被回收。
此外,Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
由于操作数存储在内存中,频繁的内存读/写操作会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存技术(TOS,Top-of-Stack Cashing),将栈顶元素缓存在物理 CPU 的寄存器中,减少对内存的读写次数,提升执行引擎的效率。
常量池的作用就是为了提供一些符号和常量,便于指令识别。
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
当一个字节码文件被加载到 JVM 内部时,如果调用的目标方法在编译期可知,并且在运行时保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接。
如果调用的方法在编译期无法确定,即只能在程序运行时将调用方法的符号引用转换为直接引用,由于这种转换过程具有动态性,因此称之为动态链接。
-
方法绑定机制包括早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是指在符号引用被替换为直接引用的过程,仅发生一次。 -
早期绑定指被调用的目标方法在编译期可知且在运行期保持不变时,将该方法与其所属的类型进行绑定。由于目标方法明确,可以使用静态链接将符号引用转换为直接引用。 -
晚期绑定发生在被调用方法无法在编译期确定时。在程序运行期根据实际类型绑定相关方法,这称为晚期绑定。
非虚方法指在编译期确定具体调用版本,在运行时不可变的方法。这包括静态方法、私有方法、final方法、实例构造器以及父类方法。而编译期无法确定的方法则称为虚方法。
普通调用指令固化在虚拟机内部,方法的调用执行不可认为干预,而动态调用指令则支持由用户确定版本方法。其中invokestatic和invokespecial指令调用方法称为非虚方法,其余的(final修饰除外)称为虚方法。
Java 7中对动态语言类型支持的改进实质上是对Java虚拟机规范的修改,而非Java语言规则的修改。这一领域较为复杂,主要增强了虚拟机中的方法调用机制,直接受益者是运行在Java平台上的动态语言编译器。
动态类型语言和静态类型语言的主要区别在于类型检查时机。静态类型语言在编译期进行类型检查,而动态类型语言在运行期进行类型检查。简言之,静态类型语言根据变量自身的类型信息进行检查,而动态类型语言根据变量值的类型信息进行检查。这突显了动态语言的一个重要特征:变量没有类型信息,变量值才有类型信息。
查找操作数栈顶元素的实际类型(记为C),并与常量中的描述符和简单名称匹配的方法。若权限验证通过,则返回该方法的直接引用,结束搜索过程;否则,抛出java.lang.IllegalAccessError异常。如果未找到匹配的方法,则按照继承关系逐级向上搜索C的父类,并重复上述验证过程。若始终未找到合适的方法,则抛出java.lang.AbstractMethodError异常。
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
-
在面向对象编程中,频繁使用动态分派可能导致在类的方法元数据中反复搜索适当的目标,影响执行效率。为提高性能,JVM采用在类的方法区建立虚方法表(virtual method table)的方式来解决这一问题。虚方法表存储着各个方法的实际入口,并使用索引表代替查找。每个类都有自己的虚方法表,其中记录了该类及其父类的所有可继承方法的实际入口。非虚方法不会出现在虚方法表中。 -
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成后,JVM会把该类的方法表也初始化完毕。
-
执行引擎遇到返回指令(如ireturn、lreturn、freturn、dreturn、areturn用于不同返回值类型,以及void方法的return指令),将返回值传递给调用者,称为正常完成出口。返回指令的选择根据返回值的数据类型确定。 -
方法执行过程中遇到未处理异常,即在方法的异常表中找不到匹配的异常处理器,导致方法退出,称为异常完成出口。异常处理信息存储在异常处理表中,便于异常发生时找到相应的处理代码。
-
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。 -
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
举例栈溢出的情况
调整栈的大小,就能保证不出现溢出吗?
分配的栈内存越大越好么
垃圾回收是否会涉及到虚拟机栈
方法中定义的局部变量是否是线程安全的
具体问题具体分析:变量是在内部产生,且在内部消亡的则是线程安全的,否则不是线程安全的。
原创文章,作者:速盾高防cdn,如若转载,请注明出处:https://www.sudun.com/ask/76928.html