jvm堆栈分析,栈帧java
题目:jvm学习第4天——栈帧(所有Java虚拟机数据都以栈帧格式存在)学习内容:
1、栈帧的内部结构
2、 局部变量表
3、 操作数栈
4、 动态链接(或指向运行时常量池的方法引用)
5、方法返回地址(或方法正常退出或者异常退出的定义)
6、关于栈的几个问题
7、方法的调用
内容:
1、栈帧的内部结构
每个栈帧中存储着:
局部变量表(Local variables)
操作数堆栈(或表达式堆栈))。
动态链接(或引用运行时常量池的方法))。
方法返回地址)或方法成功或异常终止的定义)。
一些堆栈帧也包含
一些附加信息
。例如,一些信息支持程序调试。它由五部分组成。
其中方法返回地址和动态链接和一些附加信息,一般又叫帧数据区。
2、 局部变量表
局部变量表也叫局部变量数组或局部变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型。
局部变量表建在线程的栈中,是线程的特殊数据,所以不存在数据安全问题。
在
局部变量表所需的容最大小是在编译期确定下来的
中,它保存在方法的Code属性的最大局部变量数据项中。在方法执行期间,局部变量表的大小不变。对该方法的嵌套调用的数量由堆栈的大小决定。一般来说,堆栈越大,方法的嵌套调用就越多。对于函数来说,参数和局部变量越多,局部变量表就越膨胀,堆栈框架就越大,以满足方法调用所需的信息增长的需要。此外,函数调用会占用更多的堆栈空间,从而减少嵌套调用的数量。
局部变量表中的变量仅对当前方法调用有效。当该方法运行时,虚拟机使用局部变量表将参数值传送到参数变量列表。方法调用后,局部变量表将与方法堆栈中的框架一起被丢弃。
局部变量表,最基本的存储单元是slot (变量槽)
参数的存储总是从局部变量数组的索引index0开始,以数组长度为-1的索引结束。
局部变量表存储各种基本数据类型(8种类型)和编译时已知的引用类型。(引用),一个返回地址类型的变量。
在局部变量表中,小于32位的类型占用1个槽(包括返回地址类型),64位的类型(long和double)占用2个槽。
字节、短字符和字符在保存前转换为整数,布尔值也转换为整数。0表示假,0表示真。
在JVM的局部变量表中为每个槽分配一个访问索引。使用此索引可以成功访问局部变量表中指定的局部变量的值。
当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量将被依次复制到局部变量shell中的每个槽中。
如果需要访问局部变量表中的64位局部变量值,只需使用前面的参数。例如,访问long或double类型的变量。)
如果当前帧是由构造方法或实例方法创建的,则对象引用this存储在索引为0的槽中,其余参数按参数表的顺序排列。
可以看到此时的this的index值为0,其余的对象继续排列。
栈中局部变量表中的槽位是可重用的。一旦一个局部变量通过了它的值域,hxsdqz的新局部变量很可能会重用值域后过期的局部变量的槽位,从而达到节省资源的目的。
b刚开始的index值是2,说明这个槽位b用过,但是由于某些原因,b不用了,由于这是个数组,大小不变,所以下一次用,还是用的index为2的这个槽位,可以看到c最后用的也是index为2的槽位。
参数表分配后,根据方法中定义的变量顺序和范围进行分配。
类变量表有两次初始化的机会,第一次是在链接的“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
强烈的
补充说明
在堆栈框架中,与性能调优最密切相关的部分是前面提到的局部变量表。当执行该方法时,虚拟机使用局部变量表来完成该方法的转移。
局部变量表中的变量也是重要的垃圾收集根节点,只要局部变量表中直接或间接引用的对象不会被收集。
3、 操作数栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作堆栈,并在方法执行期间,根据字节码指令将数据写入或提取到堆栈中。数据,也就是push)/pop。
一些字节码指令将值推入操作数堆栈,而另一些则从堆栈中取出操作数。在使用它们之后,将结果推送到堆栈上。
比如复制、交换、求和等。
操作数堆栈是JVM执行引擎的一个工作空间。当一个方法刚开始执行时,会创建一个新的堆栈框架,这个方法的操作数堆栈是空的。
每个操作数堆栈都有一个用于存储值
其所需的最大深度在编译期就定义好了
的显式堆栈深度,该值保存在方法的Code属性中,是max_ stack的值。堆栈中的任何元素都是任意的Java数据类型。
3位类型占用一个堆栈单位深度。
6位类型占用两个堆栈单元深度。
操作栈不通过访问索引来访问数据,而只能通过标准的push和pop操作来访问数据一次。
如果被调用的方法有返回值,它的返回值将被压入当前堆栈帧的操作数堆栈,下一条要在PC寄存器中执行的字节码指令将被更新。
操作栈中元素的数据类型必须严格匹配字节码指令的顺序,在编译时由编译器验证,在类加载过程中的类检查阶段的数据流分析阶段再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中栈指的是操作数栈。
上面的描述都是很官方概念上的描述,比较抽象,但很重要,下面用一组图片来展示过程。
先将数据15入栈,然后出栈到局部变量表,8也入栈,然后出栈到局部变量表,然后将局部变量表中的8和15入栈,然后8,15出栈进行相加的操作,此时涉及到了与执行引擎的交互,通过执行引擎将相加的字节码指令翻译为机器指令让CPU执行运算得到23入栈,然后出栈到局部变量表。
最后一张图片描述的是操作数栈的大小,和局部变量表的大小,从上图可以看到栈最多就是8和15同时存在,所以大小是2,而局部变量表里面有3个数据,加上index为0的this,所以大小为4,这个例子就很好的体现了上述的抽象文字
栈顶缓存技术
如前所述,基于栈架构的虚拟机使用的零地址指令更加紧凑,但是当完成一个操作时,必须使用更多的推入和推出指令,这也意味着将需要更多的指令调度次数和内存读写次数。
由于操作数存储在内存中,频繁的内存读写操作必然会影响执行速度。为了解决这个问题,Hotspot JVM的设计者提出了ToS (Top-of-Stack Caching,栈顶缓存)技术,将所有的柜元素缓存在物理CPU的寄存器中,以减少对内存的读写次数,提高执行引擎的执行效率。
我们先来了解一下这个。这是刚刚提出的。还需要测试,不过以后应该会实现。
4、 动态链接
每个堆栈框架都包含对运行时常量池中堆栈框架的方法的引用。该引用的目的是支持当前方法代码的动态链接。例如:invokedynanic指令
当Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用存储在类文件的常量池中。比如一个方法调用另一个方法时,在常量池中用指向该方法的符号引用来表示,那么动态链接的作用就是把这些符号引用转换成调用方法的直接引用。
如果引用,就用#几。很方便。
5、方法返回地址
存放调用该方法的pc寄存器的值。
一种方法的结束,有两种方式:
正常执行完成。
出现未处理的异常,并出现异常退出。
无论以哪种方式退出,都将在方法退出后返回到调用该方法的位置。当方法正常退出时,调用方PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。但是,如果存在异常,则返回地址由异常表确定,并且该信息一般不保存在堆栈框架中。
6、关于栈的几个问题
举个栈溢出的例子?(堆栈0溢出错误)
通过-Xss设置堆栈的大小;00M
通过调整堆栈大小,能保证不会溢出吗?
不会,栈还是有最大极限的,有些无限循环总会突破这个极限。
分配的堆栈内存越大越好?
不要!整个内存空间好大。如果这个堆栈很大,线程也会更大,堆叠的空间也会更多,从而导致线程更少。
垃圾回收会涉及虚拟机栈吗?
不要!
方法中定义的局部变量是线程安全的吗?
如果线程是私有的,那么它是安全的,但是共享的线程则不是。
7、方法的调用
在JVM中,符号引用转换为调用方法的直接引用与方法的绑定机制有关。
静态链接:
当一个字节码文件被加载到JVM中时,如果被调用的目标方法在编译时是已知的,并且运行时保持不变。在这种情况下,将调用方法的符号引用转换为直接引用的过程称为静态链接。
动态链接:
如果被调用的方法在编译时无法确定,也就是说被调用方法的符号引用只能在程序运行时转换成直接引用。因为这个引用转换过程是动态的,所以也称为动态链接。
对应方法的绑定机制是早期绑定和晚期绑定。绑定是一个用直接引用替换字段、方法或类的符号引用的过程,这种情况只发生一次。
早期绑定:
早期绑定意味着,如果被调用的目标方法在编译时是已知的,而运行时保持不变,那么这个方法可以被绑定到它的类型。这样,由于调用哪个目标方法是明确的,所以可以通过静态链接将符号引用转换为直接引用。
晚期绑定:
如果在编译时无法确定被调用的方法,那么只能在程序运行时根据实际类型绑定相关的方法,这就是所谓的后期绑定。
随着高级语言的出现,类似Java- like的面向对象编程语言越来越多。虽然这些编程语言在语法风格上有一定的差异,但它们总是有一个共同的特点,那就是都支持封装、继承、多态等面向对象的特性。这种编程语言既然有多态性,自然就有早期绑定和晚期绑定两种绑定方式。
事实上,Java中任何一个常用的方法都具有虚函数的特性,虚函数相当于C语言中的虚函数(C中需要显式定义关键字virtual 1)。如果不希望一个方法在Java程序中具有虚函数的特征,可以用关键字final来标记这个方法。
非虚方法:
如果方法在编译时确定了具体的调用版本,那么这个版本在运行时是不可变的。这样的方法叫做非虚方法。
静态方法、私有方法、最终方法、实例构造函数和父方法都是非虚方法。
其他方法称为虚拟方法。
使用子类多态性的前提:子类的继承关系方法的重写。
虚拟机中提供了以下几条方法调用指令:
普通呼叫指令:
Invokestatic:调用静态方法,在解析阶段确定纯方法版本。
Invokespecial:调用方法,私有和父方法,在解析阶段确定唯一的方法版本。
KeVIVOIRTUAL:调用所有虚方法。
Invokeinterface:调用接口方法
动态调用指令:
Invokedynamic:动态解析要调用的方法,然后执行。
前四条指令固化在虚拟机内部,方法无法人为干预调用和执行,而invokedynamic指令支持用户确定方法版本。invokestatic和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
JVM字节码指令集一直比较稳定,直到Java7增加了invokedynamic指令,这是Java为了支持动态类型语言而做的改进。
但是在Java7中,没有办法直接生成i nvokedynamic指令,需要ASM这种底层字节码工具来生成invokedynamic指令。直到Java8中Lambda表达式的出现和invokedynamic指令的生成,Java中才有了直接生成模式。
Java7中加入的动态语言类型支持的本质是修改Java虚拟机的规范,而不是修改Java语言的规则。这一块比较复杂,在虚拟机中增加了方法调用。最直接的受益者是运行在Java平台上的动态语言的编译器。
动态类型语言和静态类型语言的区别在于类型是在编译时检查还是在运行时检查。如果满足前者,它就是静态类型语言,而后者是动态类型语言。
说白了,静态类型化语言就是判断变量的类型信息,动态类型化语言就是判断变量值的类型信息。变量没有类型信息,但是变量值有类型信息,这是动态语言的一个重要特性。
例如:
Java: String info=、“at guigu”;//info=atguigu;
JS:var name=" shk start ";var name=10
python:info=130.5;//info没有定义类型,但是130.5是double,所以info是double。