2.2虚拟机栈
2.1.1概述
优点:跨平台,指令集小,编译器容易实现
缺点:性能下降实现同样的工能需要更多的指令集
栈是运行时的单位,而堆是存储的单元
是什么?
每个线程在创建是辉创建一个虚拟机栈,其内部保存的一个个栈帧队里着一次次的java方法的调用。
生命周期:与线程一致。
作用:
主管java程序的运行,他保持方法的局部变量,部分结果,并参与方法的调用和返回。
特点:
快,仅次于pc寄存器,无gc,可能oom。只能进出栈两个操作。
栈的异常
stackoverflowerror 死循环
线程请求分配栈容量超过java虚拟机栈的最大容量。
outofmerroryerror动态扩增无法申请足够的内容(内存不足)
设置栈的大小 -Xss
2.1.2栈帧
栈帧中存储什么?
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在
在这个线程上正在执行的每个方法对应一个栈帧
栈帧是一个内存块,是一个数据集,维系方法执行过程中的各种数据信息。
栈运行原理:
jvm对java栈只有压栈和出栈
一条活动线程中,一个时带点只能有一个正在执行方法的栈帧,称为当前栈帧,其方法为当前方法,对饮的类为当前类。
执行引擎运行的所有字节码指令只对当前栈帧进行操作。
如果当前方法调用的了新的方法,就会创建新方法的栈帧,放在栈顶。
弹出栈帧的两个方式:
return正常函数返回,或者抛出异常。
2.1.3栈的内部结构
每个栈帧中存储:
局部变量表
操作数栈
动态链接
方法返回地址
一些附加信息
局部变量表
局部变量数据或本地变量表
用于存储方法参数和定义字啊方法体内的局部变量
关于slot的理解
局部变量表的最基本单元
32位以内的占用一个slot,64位的占用两个
long和double调用使用第一个索引,
实例方法,即非静态的方法,会那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
静态方法中没有this,不在静态变量表中存在。
局部变量表中的空间可重复利用,因为出了作用域被销毁,下面的变量即可复用。
静态变量和局部变量对比
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
我们知道类变量表有两次初始化的机会,第一次是在==“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”==阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
局部变量一定要显示赋值才能被使用。
- 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2.1.4操作数栈
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈的深度,看同时操作了多少个变量。
2.1.5栈顶缓存技术
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
2.1.6动态链接
动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
其实:字节码指令中#对应常量池中的代码。#为符号引用,边上的为真实引用。
为什么需要运行时常量池呢?
常量池的作用:就是为了提供一些符号和常量,便于指令的识别.
方法的调用:
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关.
多个栈帧可以调用项目的常量池中的引用
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
使用super.eat方法,就是调用明确的父类的方法,就是早期绑定。
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
比如接口中的方法,或者方法参数为对象的方法调用。
一般高级语言都有早期绑定和晚期绑定。封装继承多态的出现
虚方法和非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。
普通调用指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行
需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java是静态类型语言(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言
lambda表达式实现
illegalAccesserror
没有权限访问,重复加载maven的jar包,
虚方法表:
为了提高性能,JVM采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类一个虚方法表。
2.1.7方法返回地址
存放调用该方法的pc寄存器的值。
调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
当一个方法开始执行后,只有两种方式可以退出这个方法:
在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用。
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
面试题:
栈溢出的情况?栈溢出:StackOverflowError
栈中是不存在GC的,存在OOM和StackOverflowError
举个简单的例子:在main方法中调用main方法,就会不断压栈执行,直到栈溢出;
栈的大小可以是固定大小的,也可以是动态变化(动态扩展)的
如果是固定的,那么会抛出StackOverflowError
如果是动态扩展的,那么会抛出OOM异常(java.lang.OutOfMemoryError)
举例栈溢出的情况?(StackOverflowError)
通过 -Xss设置栈的大小
调整栈大小,就能保证不出现溢出么?
不能。因为调整栈大小,只会减少出现溢出的可能,栈大小不是可以无限扩大的,所以不能保证不出现溢出
分配的栈内存越大越好么?
不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
垃圾回收是否涉及到虚拟机栈?
不会;垃圾回收只会涉及到方法区和堆中,方法区和堆也会存在溢出的可能
程序计数器,只记录运行下一行的地址,不存在溢出和垃圾回收
虚拟机栈和本地方法栈,都是只涉及压栈和出栈,可能存在栈溢出,不存在垃圾回收
方法中定义的局部变量是否线程安全?
具体问题具体分析。如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。