运行时数据区
分区介绍
java虚拟机定了了若干种程序运行期间会使用到的运行时数据区.
其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的
这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
如图,灰色的区域为单独线程私有的,红色的为多个线程共享的,即
- 每个线程:独立包括程序计数器、栈、本地栈
- 线程间共享:堆、堆外内存(方法区、永久代或元空间、代码缓存)
线程
03 线程
线程是一个程序里的运行单元,JVM允许一个程序有多个线程并行的执行;
- 在HotSpot JVM,每个线程都与操作系统的本地线程直接映射。
- 当一个java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。java线程执行终止后。本地线程也会回收。
- 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用java线程中的run()方法.
大概意思就是说我们java启动的每一个线程都是和 操作系统上的线程时一一对应的关系 jvm创建线程其实是交给操作系统创建线程执行的 本地线程常见成功然后执行线程中的run方法
jvm是否终止还要看我们的线程是不是最后一个非守护线程
JVM系统线程
如果你使用jconsole或者任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用main方法的main线程以及所有这个main线程自己创建的线程;
这些主要的后台系统线程在HotSpot JVM里主要是以下几个:
虚拟机线程L这种线程的操作时需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行包括“stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
周期任务线程:这种线程是时间周期事件的提现(比如中断),他们一般用于周期性操作的调度执行。
GC线程:这种线程对于JVM里不同种类的垃圾收集行为提供了支持
编译线程:这种线程在运行时会降字节码编译成本地代码
信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理
程序计数器
作用
PC寄存器是用来存储指向下一条指令的地址所以没有规定任何OOM情况的区域,也即将将要执行的指令代码。由执行引擎读取下一条指令。
- 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
- 在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)。
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令
三个面试问题
1.使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
2.PC寄存器为什么会设定为线程私有
CPU会不停滴做任务切换,为了能够准确地记录各个线程正在执行的当前字节码指令地址,自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
虚拟机栈
概述
背景
- 由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
- 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
内存中的堆与栈
- 栈是运行时的单位,而堆是存储的单位
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
- 对象主要都是放在堆空间的,是运行时数据区比较大的一块
栈空间存放 基本数据类型的局部变量,以及引用数据类型的对象的引用
虚拟机栈是什么
-
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这个一次次的java方法调用。它是线程私有的, 生命周期和线程是一致的
-
作用:管理java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
局部变量:是从属于方法的变量
基本数据变量: 相对于引用类型变量(类,数组,接口)
栈的特点
- 栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器(程序计数器)
- JVM直接对java栈的操作只有两个
- 每个方法执行,伴随着进栈(入栈,压栈)
- 执行结束后的出栈工作
- 对于栈来说不存在垃圾回收问题
栈中可能出现的异常
- java虚拟机规范允许Java栈的大小是动态的或者是固定不变的 (其实就是说一个在编译时出现问题一个运行时出现问题)???
- 如果采用固定大小的Java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 StackOverFlowError异常
- 如果采用java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常
/**
* 演示栈中的异常
*/
public class StackErrorTest {
public static void main(String[] args) {
main(args);
}
}
设置栈的内存大小
我们可以使用参数**-Xss选项来设置线程的最大栈空间**,栈的大小直接决定了函数调用的最大可达深度。
(IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k)
/**
- 演示栈中的异常
- 默认情况下:count 10818
- 设置栈的大小: -Xss256k count 1872
*/
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
-Xss256K: 设置每个线程的运行时虚拟机栈的大小为 256K。
-Xmx,设置JVM最大内存;比如 -Xmx512M: 设置JVM最大内存为512M;
-Xms,设置JVM最小内存;比如 -Xms512M: 设置JVM最小内存为512M;
-Xmn,设置JVM年轻代内存;比如 -Xmn1G:设置年轻代内存为 1 G。
虚拟机栈的组成
-
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在在这个线程上正在执行的每个方法都对应各自的一个栈帧,维系着方法执行过程中的各种数据信息
-
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame)
-
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
-
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
-
不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧
-
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
-
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出
栈帧
所以栈帧 又分为
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或执行运行时常量池的+ 方法引用)
- 方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)
- 一些附加信息
局部变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddressleixing
由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 - 局部变量表所需的容量大小是在编译期确定下来的并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
利用javap命令对字节码文件进行解析查看局部变量表
变量槽slot的理解与演示
-
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
-
局部变量表,最基本的存储单元是Slot(变量槽)
-
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
-
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true;
- long和double则占据两个slot。
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
- 如果需要访问局部变量表中一个64bit的局部变量值时(就是如果需要两个slot的槽我们就返回第一个槽的位置就可以了) ,只需要使用签一个索引即可。(比如:访问long或者double类型变量)
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。
public class LocalVariablesTest {
private int count = 1;
//静态方法不能使用this
public static void testStatic(){
//编译错误,因为this变量不存在与当前方法的局部变量表中!!!
// 静态方法就针对于类来说的 不是针对于具体的对象 所以也根本就没有局部方法表
System.out.println(this.count);
}
}
重复利用变量槽
栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
private void test2() {
int a = 0;
{
int b = 0;
b = a+1;
}
//变量c使用之前以及经销毁的变量b占据的slot位置
int c = a+1;
}
静态变量与局部变量的对比及小结
-
变量的分类:按照数据类型分:
- ①基本数据类型;
- ②引用数据类型;
-
按照在类中声明的位置分:
-
成员变量:在使用前,都经历过默认初始化赋值
-
static修饰:类变量:类加载linking的准备阶段给类变量默认赋值——>初始化阶段给类变量显式赋值即静态代码块赋值;
-
不被static修饰:实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
-
局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
-
-
补充
在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈(Operand Stack)
栈 :可以使用数组或者链表来实现
-
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以成为表达式栈
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)
- 比如:执行复制、交换、求和等操作