1. 运行时数据区
1.1 概述
运行时数据区的组成:本地方法栈、程序计数器、虚拟机栈、堆、方法区五部分组成;
每个线程私有的:程序计数器、本地方法栈、虚拟机栈;
线程间共享的:堆、方法区;
在Hotspot虚拟机中,每个线程和操作系统的本地线程直接映射,当一个Java线程准备好执行以后,此时一个操作系统的本地线程也会同时创建,Java线程执行终止后,本地线程也会被回收;
JVM虚拟机的后台系统线程:
1). 虚拟机线程:此类线程的操作需要JVM达到安全点后才会出现,线程的主要任务包括"stop-the-world"的垃圾收集、线程栈收集、线程挂起以及偏向锁撤销;
2). 周期任务线程:此类线程用于周期性操作的调度执行;
3). GC线程:此类线程负责支持垃圾回收行为;
4). 编译线程:这种线程会在运行时将字节码编译为本地代码;
5). 信号调度线程:此类线程会接收信号并且发送给JVM,在其内部通过调用适当的方法进行处理;
1.2 程序计数器
程序计数器的作用:PC寄存器用来存储指向下一条指令的地址,然后由存储引擎读取下一条指令;
程序计数器的特点:
1). 占据一块很小的内存空间,也是运行速度最快的存储区域;
2). 每个线程都有自己的程序计数器,程序计数器是线程私有的,生命周期和线程周期保持一致;
3). 任何时间一个线程都只有一个方法在执行,即当前方法;程序计数器存储当前线程正在执行的当前方法的JVM指令地址;如果是在执行native方法。则程序计数器存储的值为undefined;
4). 程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖该计数器来完成;
5). 字节码解释器正是通过改变程序计数器的值来选取下一条需要执行的字节码指令;
6). 程序计数器是唯一一个在Java虚拟机规范中没有OutOfMemoryError异常的区域;
PC寄存器为什么设置为线程私有的?
因为每个线程执行任务的进度互不相同,通过PC寄存器记录线程执行的字节码指令的地址,可以避免线程之间出现相互干扰的情况;
1.3 虚拟机栈
1.3.1 虚拟机栈概述
由于跨平台性的设计,Java指令都是基于栈来设计的;不同CPU架构所有不同,因此不能设计为基于寄存器的;
优点:跨平台,指令集小,编译器容易实现;
缺点:性能下降,实现同样的功能需要更多的指令;
Java虚拟机栈是线程私有的,每个线程创建时都会创建一个虚拟机栈,其内部保存的基本单位为栈帧,对应着一次次Java方法调用;Java虚拟机栈的生命周期和线程的生命周期保持一致;虚拟机栈主管Java程序的运行,保存方法的局部变量、部分结果、参与方法的调用和返回;
虚拟机栈的优点:
1). 栈是一种快速有效的内存分配方式,访问速度仅次于程序计数器;
2). JVM对Java栈的操作只有两个:入栈和出栈;
3). 栈不存在垃圾回收的问题;
栈中可能出现的异常:
Java虚拟机栈允许Java栈的大小是动态的或者是固定不变的;
如果采用固定大小的Java虚拟机栈,则每个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机会抛出一个StackOverflowError异常;
如果Java虚拟机栈的大小是动态的,但是在尝试扩展的时候无法申请到足够的内存,或是在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,Java虚拟机将会抛出一个OutOfMemoryError异常;
设置栈大小:通过-Xss可以设置线程的最大栈空间,栈的大小直接决定函数调用可达的最大深度;
1.3.2 栈的存储单位
Java虚拟机栈中的基本存储单位为栈帧,栈帧对应线程一次次方法的调用,维系着方法执行过程中的各种数据信息;
栈的特点:
1). Java对于栈的操作只有两个,入栈和出栈,遵循先进后出的原则;
2). 在一条活动线程中,某一个时间点上,只会有一个活动栈帧,对应当前正在执行的方法,称为当前方法;
3). 执行引擎运行的所有字节码指令只针对当前栈帧进行操作;
4). 如果在该方法中又调用了其他方法,则会有新的栈帧被创建出来,放在栈的顶端,称为新的活动栈帧;
5). 不同线程中所包含的栈帧不存在相互引用,即不可能在一个栈帧中去引用另外一个线程的栈帧;
6). Java方法有两种返回函数的方式:一种是正常的函数返回,使用return指令;一种是抛出异常;不管是哪种方式,都会导致栈帧被弹出;
栈帧的内部结构:
局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息;
1.3.3 局部变量表
局部变量表:
1). 局部变量表又被称为局部变量数组或本地变量表;
2). 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量;数据类型包括:基本数据类型、对象引用以及returnAddress类型;
3). 局部变量表位于栈帧内,是线程的私有数据,因此不存在数据安全问题;
4). 局部变量表所需的容量大小是在编译期就确定下来的,并且保存在方法的code属性的maximum local variables数据项中,在方法运行期间不会改变局部变量表的大小;
5). 方法嵌套调用的次数由栈的大小来决定,一般来说,栈越大,方法嵌套调用的次数则会越多;
6). 局部变量表中的变量只在当前方法调用中有效,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁;
slot槽的理解:
1). 参数在局部变量数组中存放总是从index0开始,到数组长度-1的索引结束;
2). 局部变量表最基本的存储单位是槽;
3). 局部变量表存放编译器可知的各种基本数据类型、引用类型、returnAddress类型的变量;
4). 在局部变量表中,32位以内的类型只占用一个slot,而64位的类型占用两个slot;
byte、short、char在存储前被转换为int,boolean也会被转换为int,0表示false吗,非0表示true;
long、double则占据两个slot:
5). JVM会为局部变量表中的每一个slot都分配一个索引,通过索引可以成功访问局部变量表中指定的局部变量值;
6). 当一个实例方法被调用时,它的方法参数和方法内定义的局部变量将会按照顺序被复制到局部变量表中的每个slot上;
7). 如果需要访问局部变量表中一个64bit的局部变量值,则只需要访问前一个索引即可;
8). 如果当前帧由构造方法或实例方法创建,则index0位置会存放对象引用this;
slot的重复利用:
栈帧中的局部变量表槽位是可以复用的,如果一个局部变量过了其作用域,则其作用域后申明的新的局部变量很可能复用过期的局部变量的槽位,从而达到节省资源的目的;
1.3.4 操作数栈
操作数栈:在方法执行过程中,根据字节码指令,向操作数栈中写入数据或从中提取数据;操作数栈主要用来保存计算的中间结果,同时作为计算过程中变量的临时存储空间;
操作数栈的特点:
1). 操作数栈的栈深度是在编译期便确定好了的,保存在方法的code属性中,为max_stack的值;
2). 32bit的数据类型会占用一个栈深度,64bit的数据类型会占用两个栈深度;
3). 操作数栈并非通过索引的方式进行数据的访问,而是通过标准的入栈和出栈的操作完成一次的数据访问;
4). 如果被调用的方法有返回值的话,则返回值会被压入到当前栈帧的操作数栈中;
5). 操作数栈中的元素数据类型必须和字节码序列严格匹配,会由编译器在编译期间进行验证,同时在类加载过程的Verification阶段会进行再次验证;
6). Java虚拟机的解释引擎是基于栈的执行引擎;
1.3.5 栈顶缓存技术
由于操作数是存储在内存中的,因此频繁的内存读/写操作必然会影响执行的速度;为了解决该问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存到物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率;
1.3.6 动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,保存该引用的目的就是为了支持当前方法的代码能够实现动态链接;动态链接的作用便是将字节码文件的常量池中记录的变量和方法的符号引用转化为直接引用;
1.3.7 方法的调用
静态链接:如果被调用的方法在编译期便可以确定下来,并且在运行期间保持不变,则这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接;
动态链接:如果被调用的方法在编译期无法确定下来,即只能够在程序运行期将调用方法的符号引用转化为直接引用,由于这种引用转换过程具有动态性,因此称为动态链接;
浙公网安备 33010602011771号