JVM 虚拟机栈

概念

java虚拟机栈是线程私有的,其生命周期和线程相同。虚拟机栈描述的是java方法执行的线程内存模型,每个方法被执行,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、参与方法的调用与返回等。每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中出入栈到出栈的过程

特点

        栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
        JVM对于栈的操作只有两个
                每个方法执行,伴随着入栈
                每个方法执行结束后的出栈
        对于栈来说不存在垃圾回收问题,会存在OOM内存溢出问题
        先进后出

栈中可能出现的异常

        java虚拟机规范允许Java栈的大小是动态或者固定不变的
        如果采用固定大小,那么一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈大小超过虚拟机栈允许的最大容量,那么Java虚拟机会抛出SOF异常。
        如果java虚拟机栈可以动态扩展,当在尝试扩展但无法申请到足够的内存时,或者在创建新的线程动态扩展时没有足够的内存去创建对应的虚拟机栈,这时就会抛出OOM异常。

设置栈的大小

        -Xss256k

栈运行原理

JVM 虚拟机栈_第1张图片

        在一条活动线程中,一个时间点上,只有一个活动的栈帧,即当前栈帧,这个栈帧对应的方法就是当前方法(方法和栈帧是一一对应关系),这个方法所在的类就是当前类
       
不同的线程中所包含的栈帧是不允许相互引用的,即不可以在一个栈帧中引用另外一个线程的栈帧。
        如果当前方法调用了其他方法,方法返回的时候,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会抛弃当前栈帧,使得前一个栈帧重新成为当前栈帧
        Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常(未处理)。不管哪一种都会导致当前栈帧的弹出。

栈帧的内部结构

        1、局部变量表(Local Variables)
        2、操作数栈(Operand Stack)(或表达式栈)
        3、动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
        4、方法返回地址(Return Address)(或方法正常退出或异常退出的定义)
        5、一些附加信息

局部变量表

        局部变量表也被称之为局部变量数组或本地变量表
        定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型主要包括各种基木数据类型、对象引用(reference),以及returnAddress (方法返回)类型。
        由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
        局部变量表所需的容量大小是在编译期确定下水的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
        方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。说人话就是栈里栈帧的多少由栈帧的大小决定,而栈帧的大小由栈帧里面的结构大小决定(主要是局部变量表的大小),而局部变量表的大小由方法的参数和局部变量所决定
        局部变量表中的变量只在当前方法调用中有效。
在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后随着方法栈帧的销毁,局部变量表也会随之销毁。

字节码中方法内部结构
JVM 虚拟机栈_第2张图片

关于Slot(变量槽)
        局部变量表最基础的存储单元就是Slot
        如果是非静态方法,那么槽的第一个位置就是"this"
        byte、short、char、在存储前被转换成int,boolean也被转换成int,0表示false,非0表示true

关于静态变量和局部变量的对比
       
类变量有两次初始化的机会,一次在准备阶段赋类型默认值,一次在初始化阶段赋程序员设的值。和类变量初始化不同,局部变量不存在系统初始化的过程,这意味着局部变量必须人为赋值才是使用
        变量的分类
                按照类型分可以分成:基本数据类型变量、引用数据类型变量
                按照在类中的位置分:成员变量、局部变量
                        成员变量被static修饰被称为类变量(静态变量)
                        没被static修饰被称为实例变量(随着对象的创建,在堆中分配空间,并进行默认赋值)

操作数栈

        它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
        操作数栈是以数组实现的,但是不能通过索引访问元素,只能通过出栈/入栈操作。并且数组一旦创建其大小就不再改变,换句话说就在方法对应的栈帧创建时其里面的操作数栈深度就已确认
        操作数栈只有出栈/入栈的存在。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。譬如在做算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递。
        
在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在
大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了

JVM 虚拟机栈_第3张图片

以下从静态方法和非静态方法的字节码图解说明操作数栈的操作
静态方法:
        JVM 虚拟机栈_第4张图片

JVM 虚拟机栈_第5张图片

非静态方法:
        JVM 虚拟机栈_第6张图片

栈顶缓存技术

        栈架构指令集(零地址指令)的代码虽然紧凑,但是完成相同功能所需的指令数量一般会比寄存器架构来得更多,因为出栈、入栈操作本身就产生了相当大量的指令。更重要的是栈实现在内存中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。尽管虚拟机可以采取栈顶缓存的优化方法,把最常用的操作映射到寄存器中避免直接内存访问,但这也只是优化措施而不是解决本质问题的方法。因此由于指令数量和内存访问的原因,导致了栈架构指令集的执行速度会相对慢上一点
        由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者提出了栈顶缓存(ToS, Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写操作,提升执行引擎的执行效率。

动态链接(或指向运行时常量池的方法引用)

        运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
        每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

JVM 虚拟机栈_第7张图片

方法的调用

        一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。
        编译期可知,运行期不可变:静态方法、私有方法、实例构造器、父类方法(super调用)、final修饰的方法。各自的特点决定了它们都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。所以它们也叫非虚方法。其它的方法就是虚方法
        
静态链接:编译期可知,运行期不可变,调用方法的符号引用转化为直接引用就叫静态链接
        动态链接:调用的方法在编译时期无法确认,只能在运行时期将符号引用转化为直接引用,具备动态,就叫动态链接

        绑定:是一个字段、方法、类在符号引用被替换成直接引用的过程,仅仅发生一次
        早期绑定:指被调用的方法如果在编译期可知,运行期不可变时,即可将这个方法与所属的类型进行绑定,这样一来明确了被调用的目标方法究竟是哪一个,因此就可以使用静态链接的方式将符号引用转换成直接引用
        晚期绑定:如果被调用的方法无法在编译期确定下来,只能在程序运行时根据实际的类型绑定相关方法,就称为晚期绑定

Java虚拟机支持以下5条方法调用字节码指令,分别是:
        普通调用
        ·invokestatic。用于调用静态方法。
        ·invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。
        ·invokevirtual。用于调用所有的虚方法。
        ·invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。
        动态调用
        ·invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接生成的方式
        只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其他方法就被称为“虚方法”(Virtual Method)。

方法重写的本质:
        子类对象的多态的使用前提:类的继承关系、方法重写
        
JVM 虚拟机栈_第8张图片

 找到方法->进行权限校验->通过就返回直接引用,否则返回异常
 没找到方法->按照继承关系找到父类,依次进行上面的操作

虚方法表
JVM 虚拟机栈_第9张图片

 在类加载的链接阶段的第三步解析的时候创建虚方法表,保存到类的方法区中

        虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址

 例子JVM 虚拟机栈_第10张图片

方法返回地址

        方法的两种退出方式:
                正常调用完成:是执行引擎遇到任意一个方法返回的字节码指令
                异常调用完成:方法执行的过程中遇到了异常,并且这个异常没有处理。只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。
        无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法(调用当前方法的方法称为调用者或者主调方法)的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
        方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令

你可能感兴趣的:(JVM,java)