3.1、概述
主要包括方法区
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应,这些线程对应的数据区域会随着线程开始和结束而创建和销毁
3.2、线程
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行运行。
在HotSpot JVM里,每个线程都与操作系统的本地线程直接映射。当一个java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。java线程执行终止后,本地线程也会回收
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用java线程中的run()方法
Java内存模型是由《Java语言规范》定义的,用来描述多线程程序语义执行的规则
JVM运行时数据区是由《Java虚拟机规范》定义的,用来描述java虚拟机执行时内存的特点
每个JVM只有一个RunTime实例。即为运行时环境
3.3、程序计数器(程序计数寄存器)
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行。
JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
3.3.1、作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
-
他是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成
3.3.2、特点
-
唯一一个在JVM规范中没有规定任何OOM情况的区域,没有GC(垃圾回收机制)
-
是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的内存区域
-
在jvm规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的是生命周期一致
-
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器都会存储当前正在执行的java方法的jvm指令地址;或者,如果当前方法是本地方法(native方法),存储的是undefined
3.3.3、问题
使用PC寄存器存储字节码指令地址有什么用或者为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停的切换各个线程,这时候切换回来后,就得知道接着从哪开始继续执行。JVM字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
3.4、虚拟机栈
3.4.1、概述
-
背景:由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。基于栈来设计,优点是可以跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
-
栈是运行时的单位,而堆是存储的单位
-
Java虚拟机栈在每个线程创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应一次次的方法调用
-
JAVA虚拟机栈是线程私有的,生命周期与线程的生命周期一致
-
主管java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回
3.4.2、特点
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
-
JVM直接对Java栈的操作只有两个:方法执行入栈,执行结束出栈
-
不存在垃圾回收,但存在OOM和StackOverflowError
3.4.3、栈中可能出现的异常
JVM虚拟机规范允许Java栈的大小是动态的或者是固定大小不变的
-
StackOverflowError:如果采用固定大小的栈,那每一个线程的java虚拟机栈的容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个StackOverflowError异常
-
OutOfMemoryError:如果java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,java虚拟机会抛出一个OutOfMemoryError异常
3.4.4、设置栈内存的大小
使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
3.4.5、栈的存储单位
-
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
-
线程上正在执行的每个方法各自对应一个栈帧
-
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
3.4.6、栈的运行原理
-
一条活动的线程中,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈顶栈帧是有效的,这个称为当前栈帧,对应方法是当前方法,对应的类称为当前类
-
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
-
如果方法中调用里其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧
-
-
不同线程中所包含的栈帧是不允许存在相互引用的,既不可能在一个栈帧之中引用另外一个线程的栈帧
-
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
-
java方法有两种返回函数的方式,一种是正常函数返回,使用return指令;另一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出
3.4.7、栈帧的内部结构
每个栈帧中存储着:局部变量表(Local Variables)、操作数栈(Operand Stack)(或表达式栈)、动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)、方法返回地址(Rturn Address)(或方法正常退出或者异常退出的定义)、一些附加信息
3.4.7.1、局部变量表(Local variables)
-
局部变量表也称之为局部变量数组或本地变量表
-
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型
-
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
-
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
-
方法嵌套调用的次数有栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,他的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
-
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量也会随之销毁
-
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
-
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference)、returnAddress(方法返回值)类型的变量
-
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot(byte、short、char在存储前被转换为int,boolean 也被转换为int,0表示false,非0表示true;long和double则占据两个slot)
-
最基本的存储单元是slot(变量槽)
-
关于Slot
-
JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
-
当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
-
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如访问long或double类型变量)
-
如果当前帧是由构造方法或者实例方法创建的,那么该对象应用this将会存放在index为0的slot处,其余的参数按照参数列表顺序继续排列
-
slot的重复利用:栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
-
-
-
补充:在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递;局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
3.4.7.2、操作数栈(Operand Stack)
-
每一个独立的栈帧中除了包含局部变量表以外,还包含一个先进后出(LIFO)的操作数栈,也可以称为表达式栈
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
-
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数栈取出栈。使用它们后再把结果压入栈。
-
比如:执行复制、交换、求和等操作
-
-
-
操作数栈主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
-
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
-
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值(https://blog.csdn.net/u014629433/article/details/51626686)
-
栈中的任何一个元素都是可以任意的java数据类型(32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位深度)
-
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈出栈操作来完成一次数据访问
-
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数中,并更新PC寄存器中下一条需要执行的字节码指令
-
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
-
java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
-
栈顶缓存技术:基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
3.4.7.3、动态链接(Dynamic Linking)
-
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如:invokedynamic指令
-
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用,类的加载过程中的链接的解析阶段的功能是将常量池内的符号引用转换为直接引用
-
常量池和运行时常量池:常量池在字节码文件中;运行时常量池在运行时的方法区中
3.4.7.4、方法返回地址(Return Address)
-
存放调用该方法的PC寄存器的值
-
无论出现哪种退出方式,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息
-
方法结束有两种方式:正常执行完成;出现未处理异常,非正常退出
-
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
-
一个方法在正常调用完成之后究竟需要哪一个返回指令还需要根据方法返回值的实际数据类型而定。
-
在字节码指令中,返回指令包含ireturn(当返回值是Boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明void方法、实例初始化方法、类和接口的初始化方法使用
-
-
在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口
-
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
-
-
本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
-
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给它上层调用者产生任何的返回值
3.4.7.5、一些附加信息
允许携带与java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息