赞
踩
原文章:阿Q说代码 : 20张图助你了解JVM运行时数据区,你还觉得枯燥吗?
个人学习
(1)为什么要使用PC寄存器记录当前线程的执行地址呢?
JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,这时候就需要PC寄存器来记录某个线程的字节码执行位置,如果虚拟机是单线程也就没必要用程序计数器记录每个线程的位置了。
(2)PC寄存器为什么会被设定为线程私有呢?
由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是为每一个线程都分配一个PC寄存器。这样各条线程之间计数器互不影响,独立存储。
堆和栈的区别
栈:运行时的单位,它解决的是程序运行的问题,即程序如何执行,或者说是如何处理数据
堆:存储的单位,它解决的是数据存储的问题,即数据怎么放、放在哪
早期叫Java栈,每个线程在创建时都会创建一个虚拟机栈,所以虚拟机栈是线程私有的,当线程结束时虚拟机栈也就结束了。
JVM对虚拟机栈的操作只有进栈和出栈,所以它的
访问速度仅次于程序计数器
,也是一种快速有效的分配存储方式。虚拟机栈来说它不存在垃圾回收问题,但是虚拟机栈的
大小是动态的或者固定不变的
,所以它会存在栈溢出或者内存溢出问题
固定大小
的虚拟机栈,那每一个线程的虚拟机栈容量可以在线程创建的时候独立选定。
StackOverflowError
异常动态扩展
,并且在尝试扩展的时候无法申请到足够的内存
,或者在创建新的线程
时没有足够的内存去创建对应的虚拟机栈,那虚拟机将会抛出OutOfMemoryError
异常。
-Xss
参数来配置栈内存,追加字母k或K表示KB,m或M表示MB,g或G表示GB,示例:-Xss1m
。虚拟机栈主管Java程序的运行,保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回,那它内部到底是什么构造呢?虚拟机栈
内部保存
着一个一个的栈帧(Stack Frame
),每个栈帧与该线程正在执行的每个方法都是一一对应的。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。
即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为**当前栈帧 **(
Current Frame
),与当前栈帧相对应的方法就是当前方法(Current Method
),定义这个方法的类就是当前类(Current Class
)。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
执行过程:
先进先出、后进后出
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令(包含
void
返回类型);一种是抛出异常(指的是未处理的异常,如果是try...catch
过了,算第一种)。不管使用哪种方式,都会导致栈帧出栈
。不同线程中所包含的栈帧是不允许存在互相引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
局部变量表也被称之为局部变量数组或本地变量表,实际上是一个“数字”数组,主要用于存储方法的参数和定义在方法体内的局部变量(包括各类基本数据类型、对象引用、
returnAddress
类型),虚拟机使用局部变量表完成方法返回。因为局部变量表是建立在线程的虚拟机栈上,是线程的私有数据,所以不会存在数据安全问题。
另外
栈帧的大小主要受局部变量表的影响
,而局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code
属性的maxmum_local_variables
数据项中,所以在方法运行期间是不会改变局部变量表的大小的,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。在虚拟机栈大小固定的前提下,它的局部变量表越大,它的栈帧就越大,那它的嵌套调用次数(方法调用数)也就越少,即栈的深度越浅
局部变量表中的数据只有在当前方法中有效。
在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
参数的存放总是在局部变量数组的索引0开始,到数组长度减1的索引结束,它最基本的存储单元就是Slot
(变量槽)。
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot
上。
JVM会为局部变量表中的每个Slot
都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。其中32位以内的类型只占用一个slot
(包含returnAddress
类型,byte
、short
、char
、float
都转化为int
类型,而boolean
类型是0为false
,非0为true
),64位的类型(long
和double
)占用两个slot
。如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。
如果当前帧是由构造方法或者实例方法创建的
,那么该对象的引用this
将会存放在index
为0的slot
处,其余的参数按照参数表顺序继续排列,而this
变量不存在于静态方法的局部变量表中,所以上文中的main
方法中不存在this
变量。
另外Slot
是可以复用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用该局部变量的slot,从而达到节省资源的目的。
补充知识点:变量按照在类中的位置可以分为成员变量和局部变量,其中成员变量又分为类变量和实例变量。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被回收。
操作数栈又称为表达式栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈。
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时
的存储空间。
每一个操作数栈都会拥有一个明确的栈深度用于存储数据值,其所需要的最大深度在编译期间就定义好了,保存在方法的code
属性中,为max_stack
的值(与上边局部变量表类似)。
栈中的元素可以是任意的Java数据类型,其中32bit的用一个栈单位深度,64bit的用两个栈单位深度。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
我们所说的Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
阿Q特地制作了一张动态图来说明一下字节码指令执行时PC寄存器、局部变量表和操作数栈的运行过程:
在编译期间局部变量表和操作数栈的大小已经确定了:
0
存放到PC寄存器中,此时,局部变量表和操作数栈的数据为空;bipush
时,将操作数15
放入操作数栈中,然后将PC寄存器的值置为下一条指令的执行地址,即2
;2
的操作指令时,将操作数栈中的数据取出来,存到局部变量表的1
位置,因为该方法是实例方法,所以0
位置存的是this
的值,PC寄存器中的值变为3
;8
先放入操作数栈,然后取出来存到局部变量表中,PC寄存器中的值也由3
->5
->6
;6
、7
、8
时,将局部变量表中索引位置为1
和2
的数据重新加载到操作数栈中并进行iadd
加操作,将得到的结果值存到操作数栈中,PC寄存器中的值也由6
->7
->8
->9
;istore_3
,将操作数栈中的数据取出存到局部变量表中索引为3
的位置,执行return指令,方法结束。如果被调用的方法带有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。
栈顶缓存技术:将栈顶的元素全部缓存到物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。
当字节码文件被加载到虚拟机后,字节码文件中的一些数据,如类型信息、域信息、方法信息等,就会被放置到方法区中,而字节码文件中的常量池则会进入方法区中的运行时常量池。每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
动态链接就是在“类加载”中“链接”的“解析阶段”将符号引用转化为直接引用的过程。
为什么字节码文件需要常量池?因为
字节码文件需要数据支持
,通常这种数据会很大,以至于不能直接存放到字节码中,换一种方式,可以将指向这些数据的符号引用存到字节码文件的常量池中,这样字节码只需使用常量池就可以在运行时通过动态链接找到相应的数据并使用。
方法返回地址是用来存放调用该方法的PC寄存器的值的。
方法结束两种方式:
无论哪种方式退出,在方法退出后都返回到该方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。
方法正常退出时,当前线程的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前
栈帧出栈
的过程需要恢复上层方法的局部变量表、操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器的值等,让调用者方法继续执行下去。
按照方法完成出口方式的不同又分为正常完成出口和异常完成出口:
ireturn
(boolean
、byte
、char
、short
和int
)、lreturn
(long
)、freturn
(float
)、dreturn
(double
)、areturn
(引用类型)和return
(void、实例初始化方法、类和接口的初始化方法)。两种方式的本质区别就是异常完成出口退出时不会给他的上层调用者产生任何的返回值。
提示:
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。
本地方法是不在运行时数据区中的,如栗子:
本地方法其实就是java调用非java代码的接口,该接口由非java语言实现。本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。
native
可以与所有其他的java标识符连用,但是abstract
除外。
Native Method
?本地方法栈是用来管理本地方法的调用的,也是线程私有的。他也允许被实现成固定或者可动态扩展的内存大小,在内存溢出方面与虚拟机栈类似。本地方法栈的具体做法是Native Method Stack
中登记native
方法,在Execution Engine
执行时加载本地方法库。
当某个线程调用本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界,他和虚拟机拥有同样的权限:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。