JVM之运行之数据区
文章目录

内存是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请,分配,管理的策略,保证了JVM的高效稳定运行。 不同的JVM对于内存的划分方式和管理机制存在着部分差异
java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另外一些则是与线程一一对应,这些线程对应的数据区域会随着线程开始和结束而创建和销毁
如上图所示:灰色的为单独线程私有,红色的为多个线程共享
每个线程:独立包括程序计数器,栈,本地库
线程间共享:堆,堆外内存
线程:
在HotSpot JVM里,每个线程都与操作系统的本地线程直接映射。
-----当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建,Java线程执行终止后,本地线程也会回收
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用Java线程中的run()方法
使用jconsole调试工具,可以看到后台有许多线程在运行(不包括调用main()方法以及所有这个main线程自己创建的线程)
JVM里面的后台线程主要有:
- 虚拟机线程
- 周期任务线程
- GC线程
- 编译线程:在运行时会将字节码编译到本地代码
- 信号调度线程:接收信号发送给JVM,在它内部通过调用适当的方法进行处理
一,PC寄存器(Program Counter Register)
JVM 中的PC寄存器是对物理pc寄存器的一种抽象模拟
作用:用来存储指向下一条指令的地址,由执行引擎取下一条指令
- pc寄存器是一块很小的内存空间,运行速度最快
- 每个线程都有自己的程序计数器,是线程私有的,生命周期与线程保持一致
- 任何时间一个线程都只有一个方法在执行,即当前方法,程序计数器存储当前线程正在执行的Java方法的JVM指令地址,如果是执行native,则是未指定值
- 字节码解释器工作时就是通过改变这个计数器来选取下一条需要执行的字节码指令
- 唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
使用PC寄存器存储字节码指令地址有什么作用?
(1)因为cpu需要不停的切换各个线程,可以使用它记录执行的位置
(2)字节码解释器工作时需要通过改变这个计数器来选取下一条需要执行的字节码指令
PC寄存器为什么被设定为私有?
为了能够准确记录各个线程正在执行的当前字节码指令地址,因为cpu根据时间片不断进行任务切换
内存中的栈与堆
栈是运行时的单位,而堆是存储的的单位:栈解决程序运行问题如:程序如何执行,如何处理数据;堆解决数据存储问题:数据怎么放,放在哪儿
二,虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法的调用,是线程私有的
生命周期与线程一致
作用:主管Java程序的运行,他保存方法的局部变量,部分结果,并参与方法的调用和返回
栈中可能出现的两个异常:
java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
(1)如果采用固定大小的Java虚拟机栈,如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,那么Java虚拟机将会抛出StackOverflowError异常
(2)虚拟机栈可以动态扩展,若没有足够的内存,会抛出OutOfMemoryError异常
设置栈内存大小,使用参数—Xss,栈的大小直接决定了函数调用的最大可达深度
栈中存储的内容:
- 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在
- 在这个线程上正在执行的每个方法都各自对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈帧的内部结构:
局部变量表(Local Variables)操作数栈(Operand Stack 或表达式栈)动态链接(Dynamic Linking 或指向运行时常量池的方法引用)方法返回地址(Return Address)一些附加信息
一,局部变量表
-
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
-
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
-
局部变量表所需的容量大小是在编译期确定下来的,保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
-
局部变量表中最基本的存储单元是Slot(变量槽)
--参数值的存放在局部变量数组的index0开始,到数组长度-1的索引结束。
-
在局部变量表里,存放编译期可知的各种基本数据类型,引用类型,returnAddress类型的变量,32位以内的类型只占用一个slot(包括 returnAddress类型),64位的类型(long和double)占用两个slot。
--byte, short, char, 在存储前被转换为int, boolean也被转换为int,0表示false,非0表示true
--long和double则占据两个slot
-
方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用次数越多(方法是一个个的入栈,如果栈太小,栈帧就会少,而且局部变量表膨胀,他的栈帧越大)
-
局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
关于slot的理解
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个访问索引即可成功访问到局部变量表中指定的局部变量值。
- 当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
- 如果需要访问局部变量表中一个64bit(long,double)局部变量值时,只需要使用前一个索引即可
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处。其余参数按照参数表顺序继续排列i。
- 帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新局部变量就很可能会复用过期局部变量的槽位,从而达到节省资源的目的。
- 在栈帧中,与性能调优关系最密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
二,操作数栈
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
- Java虚拟机的解释引擎是基于栈的执行引擎(操作数栈)
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在Code属性中,为max_stack的值
- byte,shot,char,boolean都是以int型来保存
三,动态链接
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
常量池的作用:
提供一些符号和常量,便于指令的识别
四,方法的调用:
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关
静态链接:(早期绑定,非虚方法)
目标方法在编译期可知,且运行期保持不变。(静态方法,私有方法,final方法,实例构造器,父类方法)
动态链接:(晚期绑定)
被调用的方法在编译期间无法被确定下来,只能在程序运行期根据实际的类型绑定相关的方法。
方法重写的本质:
1.找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常(程序试图访问或修改一个属性或调用一个方法,这个属性或方法你没有权限访问);
否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
虚方法表:
在面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配过程中,都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现,使用索引表代替查找
每个表都有一个虚方法表,表中存放着各个方法的实际入口
虚方法表的创建时间:
在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完毕后,JVM会把该类的方法表也初始化完毕。
方法的返回地址::
在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址,而异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息
本质上:方法正常退出后,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置pc寄存器值,让调用者方法继续执行下去。
方法在执行过程中抛出异常时不会对调用者产生任何影响,会将异常信息存储在一个异常处理表中
五个常见的面试题
1.举例栈溢出的情况
StackOverflowError 通过设置-Xss设置栈 的大小
2.调整栈大小,就能保证不出现溢栈吗?
不能
3.分配的栈的内存越大越好吗?
不是
4.垃圾回收是否会涉及到虚拟机栈?
不会
5.方法中定义的局部变量是否线程安全?
具体问题具体分析:
三,本地方法栈
本地方法:一个Native Method就是一个Java调用非Java代码的接口。这个方法不提供实现体,其实现体是由非Java语言在外面实现的。
使用Native Method方法的作用:
与Java环境外交互 如,Java与操作系统或某些硬件交换信息。
-
本地方法栈是线程私有的
-
允许被实现成固定或者是可动态扩展的内存大小(与内存溢出方面是相同的)
-
具体做法是使用Native Method Stack登记native方法,在Execution Engine执行时加载本地方法库
-
当某一个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界,他和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
不是所有的JVM都支持本地方法栈
JVM之运行之方法区