目录
内存中的堆和栈
栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
堆解决的是数据的存储问题,即数据怎么放,放在哪儿。
虚拟机栈
- 由于跨平台性的设计,Java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的。
- 栈中的数据以栈帧的格式存在,在线程上正在执行的每个方法都各自对应一个栈帧。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
优点:跨平台,指令集小,编译器容易实现。
缺点:性能下降,实现同样的功能需要更多的指令。
定义
- Java虚拟机栈也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应一次次的Java方法调用。
- 其生命周期和线程一致。
作用
主管Java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
特点
- 访问速度仅次于程序计数器。
- JVM直接对Java栈的操作只有两个:1.每个方法执行,伴随着进栈(入栈)2.执行结束后的出栈工作。
- 对于栈来说不存在垃圾回收问题。
虚拟机栈常见异常
- 如果虚拟机栈大小是固定的,此时线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果虚拟机栈可以动态扩展,并且在尝试扩展时没有请求到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,拿Java虚拟机将会抛出一个OutOfMemoryError异常。
栈帧
存储
- 局部变量表
- 操作数栈
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址
- 一些附加信息
局部变量表
定义
- 一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。其中数据类型包括八种基本数据类型,对象引用,以及ReturnAddress类。
- 不存在数据安全问题,因为局部变量表是建立在线程的栈上,是线程的私有数据。
- 局部变量表所需的容量大小是在编译期确定下来的,运行期间不会改变。
- 基本的存储单元是Slot(变量槽)
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
基本数据类型的存储
- long和double占用两个slot,其余只占用一个slot
- byte short char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
销毁
局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
Slot
- JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
- 如果需要访问局部变量表中一个64位的局部变量值,只需要使用前一个索引即可。
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
变量的分类
按照数据类型分:基本数据类型和引用数据类型
按照在类中声明的位置分:
- 成员变量:在使用前都经历过默认初始化赋值,其中分为以下两类
类变量:用static修饰,在Linking阶段默认赋值,intial阶段给类变量显式赋值即静态代码块赋值。
实例变量:随着对象的创建会在堆空间中分配实例变量空间,并进行默认赋值。 - 局部变量:在使用前,必须进行显式赋值,否则编译不通过。
操作数栈
也可以被称为表达式栈,主要是在方法执行过程中,根据字节码指令往栈中写入数据或提取数据,即入栈/出栈。
所谓Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
作用
主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
诞生
当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
存储
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了。
栈中的任何一个元素都是可以任意的Java数据类型,其中32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位深度。
栈顶缓存技术
由于栈式结构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作时必然需要使用更多的入栈和出栈指令,也意味着将需要更多的指令分派次数和内存读写次数。
由于操作数是存储在内存中的,因此频繁执行内存读写操作必然会影响执行速度,为了解决这个问题,提出了栈顶缓存技术
即将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
动态链接
定义
指向运行时常量池(其在方法区里)中该栈帧所属方法的引用,包含这个引用的目的就是支持当前方法的代码能够实现动态链接。
当Java源文件被编译为字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,比如:描述一个方法调用了另外的其它方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
作用
栈帧作为一个个方法,只存储引用而不直接存储变量和方法本身,是因为不同的方法可以调用同一个变量或者方法,如果把这些放在栈帧里面存,占的空间大且繁复,所以存引用即可。
方法的绑定机制
绑定
绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,仅仅发生一次。
静态链接
当一个字节码文件被装载进JVM内部的时候,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接。
动态链接
如果被调用的方法在编译期无法确定下来,就是说只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,就称其为动态链接。
早期绑定
指被调用的目标方法如果在编译期可知,且运行期保持不变时,就可以将这个方法与所属的类型进行绑定,因此由于明确了被调用的目标方法是哪一个,就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种方法就是晚期绑定。
方法的调用
非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。(即不能重写)
静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法。
其它方法称为虚方法。
调用指令
普通调用指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本。(调用的方法称为非虚方法)
- invokespecial:调用< init>方法,私有及父类方法,解析阶段确定唯一方法版本。(调用的方法称为非虚方法)
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
动态调用指令: - invokedynamic:动态解析出需要调用的方法,然后执行。
invokedynamic
invokedynamic是Java为了实现【动态类型语言】而做的一种赶紧,可以通过Java8的Lambda表达式进行直接生成。
动态类型语言和静态类型语言
静态类型语言是判断变量自身的类型信息,动态类型语言是判断变量值的类型信息,变量本身没有类型信息,变量值才有类型信息。
虚方法表
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
- 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
- 有了虚方法表之后,不需要每一次都向上查找是谁实现了某个方法。
- 如图,Cat类重写了:finalize() toString() eat() sayHello() sayGoodbye()等方法。
方法返回地址
存放调用该方法的pc寄存器的值。
一个方法的结束,有两种方式:1.正常执行完成 2.出现未处理的异常,非正常退出。
在方法退出后都会返回到方法被调用的位置,当方法正常退出时,调用者的pc计数器的值作为返回地址,**即调用该方法的下一条指令的地址。**而通过异常推出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息。
返回指令
当执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,即正常完成出口。
字节码指令包含:
- ireturn(返回值为boolean byte char short int)
- lreturn (long)
- freturn (float)
- dreturn (double)
- areturn (引用类型)
- return (void)
异常处理表
方法执行过程中抛出异常时的异常处理会存储在一个异常处理表里,方便在发生异常的时候找到处理异常的代码。
即如果字节码文件中第4行到第16行出了异常,就由19行来处理。
Exception table:
from to target type
4 16 19 any
19 21 19 any
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如:对程序调试提供支持的信息。
测试题
-
举例栈溢出的情况?
可以通过-Xss设置栈的大小,固定的如果不足了,会报StackOverflowError错误。如果整个空间都不足够用了,会报OOM -
垃圾回收是否会涉及到虚拟机栈?
不会 -
如何定义线程安全
如果只有一个线程才可以操作此数据,那么说这个数据是线程安全的。
如果一个变量是内部产生,内部消亡的,那么可以说是线程安全,如果有被返回之类的操作,就是不安全的。