由于跨平台的设计,导致java的指令是基于栈设计(实现与各个操作系统的低耦合性,从而实现跨平台).
优点:跨平台,指令集小,编译器容易实现
缺点:性能下降,实现同样的功能需要更多的指令
对比Java内存区中的栈和堆:
栈 | 堆 |
---|---|
解决的是程序的运行问题,即程序如何执行,或者说如何处理数据 | 解决的是数据的存储问题,即数据怎么放,放在哪里 |
Java虚拟机栈(Java Virtual Machine Stack)定义:
每个线程在创建时,都会伴随着创建一个虚拟机栈,其内部保存着一个个的栈帧(Stack Frame),对应着一次次的Java方法的调用.即可以理解为栈的内部是由一个个的栈帧组成的,而一个java方法对应着一个栈帧.
它是线程所私有的.生命周期和线程一致
作用:主管Java程序的运行,它保存方法的局部变量(即8种基本数据类型,如果是对象类型,只会保存对象的引用地址),部分结果,并参与方法的调用和返回
Java虚拟机栈的优点:
1.栈是一个快速有效的分配存储方式,访问速度仅次于程序计数器;
2.JVM直接对Java栈的操作只有两个:
每个方法调用的时候,伴随着进栈(入栈,压栈)操作,执行结束后,进行出栈操作
3.对于栈来说,不存在垃圾回收(GC)
即栈会出现StackOverflowError和OutofMemoryErrory异常
栈的存储单位
每个线程都有自己的虚拟机栈,栈中数据都是以栈帧(Stack Frame)的格式存在的;
在这个线程上正在执行的每个方法都各自对应着一个栈帧;
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息;
注意以下几点:
1.不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧;
2.如果当前方法调用了其他方法,方法返回之际,当前栈帧会传入此方法的执行结果,给前一个栈帧,接着虚拟机栈会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧;
3.Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常;不管是哪种方式,都会导致栈帧被弹出.
栈帧的内部结构
局部变量表(Local Variables)
局部变量表也被称为局部变量数组或者本地变量表
定义为一个数字数组,主要是因为其用来存储方法参数和定义在方法体内的局部变量,这些数据包括了各类的基本数据类型,对象引用,以及returnAddress类型;
由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题;
局部变量表所需的容量大小是在编译期就确定下来的,并保存在方法的Code属性的maxmum local variables数据项中.在方法运行期间不会改变局部变量表的大小
同时,请注意以下几点:
1.方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用的次数就越多.
2.局部变量表中的变量只有在当前方法调用中有效.在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法的栈帧的销毁,局部变量表也会随之销毁.
Slot槽的概念
参数值的存放总是在局部变量数组的index0开始的,到数组长度-1的索引结束.
局部变量表,最基本的单位就是Slot(变量槽)
局部变量表中存放在编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量;
在局部变量表里,32位以内的类型只会占用1个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot:
byte,short,char在存储前被转换成int,boolean也会被转换成int,0表示false,非0表示true;
long和double则占据了连个slot
因此静态方法中无法使用this变量的原因:局部变量表中没有存在this引用
注意:Slot的可重复利用性
栈帧中的sot槽是可以重复利用的,如果一个局部变量过了作用域,那么在其作用域之后,申明一个新的局部变量会复用过期的局部变量的槽位.从而达到了节省资源的目的.
细节知识点1:
类变量表有两次初始化的机会:第一次是在”准备阶段“,执行系统的初始化,对类变量设置零值,另一次则是在”初始化“阶段,赋予程序员在代码中定义的初始值;但是局部变量不存在系统初始化的过程,这也意味着一旦定义局部变量表,则必须人为地进行初始化,否则无法正常使用.
细节知识点2:
局部变量表中的变量是重要的垃圾回收根节点(GC Roots),只要被局部变量表直接或者间接引用的对象都不会被垃圾收集器回收!
操作数栈(Operand Stack)
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称为表达式栈;
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈(push)/出栈(pop)
注意:
1.如果被调用的方法带有方法值,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器下一条需要执行的字节码指令;
2.操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证.
3.Java虚拟机栈的解释引擎是基于栈的执行引擎,其中的栈就是操作数栈.
栈顶缓存技术——牛逼
由于操作数栈是存储在内存中,因此频繁地进行内存的读与写的操作必然会影响执行的速度,为了解决这个问题,HotSpot VM使用了一种栈顶缓存技术:将栈顶元素全部缓存到物理cpu的寄存器中,以此降低对内存的读/写次数,从而提升执行引擎的执行效率.
动态链接(Dynamic Linking)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
可以说:动态链接的作用就是将这些运行时常量池的符号引用转换成调用方法的直接引用
为什么存在常量池?
常量池的作用就是为了提供一些符号和常量,一方面便于指令的识别,也节省了内存的使用,若是直接将这些变量存入,将会大量占用内存空间.
方法的调用方式
在JVM中,将符号引用转换成调用方法的直接引用与方法的绑定机制有关
Java中任何一个方法都具备虚方法的特征
非虚方法:
如果方法在编译期就确定了具体的调用版本,且在运行时不可变,则这样的方法称为非虚方法;
例如:静态方法.私有方法,final方法,实例的构造器方法,父类方法;
虚方法
即是与非虚方法相反编译期不能确定了具体的调用版本,且运行时可能会发生变化
JVM中提供的方法调用指令:
方法名 | 作用 |
---|---|
invokeStatic | 调用静态方法(唯一确定) |
invokeVirtual | 调用init()方法,私有方法及父类方法等 |
invokeVirtual | 调用所有虚方法 |
invokeinterface | 调用接口方法 |
invokedynamic | 动态解析出需要调用的方法,然后执行 |
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持用户确定版本;
方法返回地址
方法的返回地址:用来存放调用该方法的pc计数器的值
一个方法的结束,有两种方式:
1.正常执行返回:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
2.出现异常信息,非正常返回:如果这个异常没有在该方法中进行处理,也就是本方法的异常表没有检测到任何匹配的异常处理器,就会导致方法退出,简称为异常完成出口
但是,不论是哪种方式退出,在方法退出后都返回到该方法调用的位置.方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址.而通过异常退出的,返回地址是要通过异常表来确定的,栈帧一般不会保存这部分信息的.
注意:正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
一些附加信息
对程序调试提供支持的信息
本地方法接口
本地方法(Native Method),就是一个java代码调用非java代码的接口:该方法的实现是由非java语言实现的,比如C/C++;
在定义一个native method时,并不提供实现体,因为其实现体是由非java语言在外面实现的;
本地接口的作用:是融合不同的编程语言为Java所用,.它的初衷是融合C/C++程序;
注意: 标有native标识符可以和所有的其他标识符连用,但是abstract除外;
为什么要使用本地方法?
Java使用虽然十分的简便,但是有些层次的任务用Java实现并不是很容易,或者效率不高,此时我们想到使用本地方法;例如:与Java环境外进行交互;与操作系统进行交互等等.
本地方法栈
Java虚拟机栈是用于管理Java方法的调用,而本地方法则是用来管理本地方法的调用
本地方法栈:也是线程所私有的
本地方法栈的特点
1.本地方法是使用C语言编写的;
2.允许被实现成固定或者可动态扩展的内存大小:
如果线程请求分配的栈容量超过了本地方法栈允许的最大容量,Java虚拟机将会抛出StackOverflawError异常;
如果本地方法栈可以动态扩展,并且在尝试扩展时,无法申请到足够的内存,或者在创建新的线程时没有足够的内存区创建对应的本地方法栈,那么Java虚拟机将会抛出OutOfMemoryError异常;
3.本地方法栈的具体实现:Native Method Stack登记native 方法,在Execution Engine执行时加载本地方法库;
4.当某一个线程调用本地方法时,它就进入了一个全新的并且不受虚拟机控制限制的世界.它和虚拟机拥有同样的权限.
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
它甚至可以直接使用本地处理器中的寄存器;
直接从本地内存中的堆中分配任意数量的内存
5.并不是所有的JVM都支持本地方法.因为Java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方法.数据结构等等.
注意:在HotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一