1.2 运行时数据区(运行时内存)
总体对比
①结构
程序计数器、本地方法栈、虚拟机栈,一个线程独有一份、
方法区、堆:一个JVM独有一份,一个进程独有一份(一个进程对应一个JVM)
②GC和OOM
GC | OOM | |
---|---|---|
程序计数器 | 无 | 无 |
虚拟机栈 | 无 | 有 |
本地方法栈 | 无 | 有 |
方法区 | 有 | 有 |
堆 | 有 | 有 |
(1)程序计数器(Program Counter Register)
线程私有、一个线程一个程序计数器(为什么需要私有? 每个线程都有自己独有的进度不能被覆盖)
主要记录的是:下一条需要解释的字节码指令的地址。(为什么需要记录? cpu需要不停地切换线程,切换后需要知道下一条执行的指令地址)
没有GC和OOM
内存空间很小,运行速度最快的存储区域
PC的值:
-
如果当前执行的是一个java方法------->下一条需要解释的字节码指令的地址
-
如果当前执行的是一个native方法------>计数器的值为空
(2)虚拟机栈Stack
①基本概念
首先:栈管运行,堆管存储(栈是运行时的单位----程序如何执行,堆是存储时的单位------数据存储、数据怎么放放在哪里)
主管java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回、线程独有
栈优点:
-
快速有效分配
-
只有出栈进栈,比较简单
-
没有GC
栈中可能出现的异常:
-
StackOverFlow:线程申请的栈容量超过了Java允许的最大容量(自己调自己---递归很容易导致栈溢出)
-
OOM:栈大小动态扩展时无法申请到足够的内存
栈的存储单元:栈帧
②栈帧
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在,每个方法对应一个栈帧,在一个时间点上只会有一个活动的栈帧(称为当前栈帧),不同线程中所包含的栈帧不允许存在相互引用
主要包括五部分:局部变量表、方法返回地址、操作数栈、动态链接、一些附加信息
局部变量表Local Variables
作用:
主要是用于存储方法参数和局部变量,定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
在栈帧中,与性能调优关系最为密切的部分:局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递(参数值到参数变量列表的传递 即实参到形参的传递)。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
数值数组,不存在数据安全问题(都是线程独有,没有共享数据) 表的大小一旦确定就不能更改
基本存储单元:
最基本的存储单元是slot
32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(1ong和double)占用两个slot。
byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。 long和double则占据两个slot
每个slot都会有一个索引,通过索引进行局部变量表中变量的访问
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。但是在static方法内不能有this(为什么? static是跟着类一起创建和消亡,而this只是一个对象,当static方法创建时this可能还未生成);
slot槽可以被重复利用(如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位)
操作数栈Operand Stack
数组实现 根据字节码指令,往操作数栈中写入数据or提取数据 栈的最大深度在编译器就被确定(数组长度不可变)
这里的操作数栈虽然是数组实现,但是不能是访问索引的方式进行数据访问(只能是出栈和入栈)
JVM是基于栈的解释器(这里的栈就是操作数栈)
栈顶缓存技术(Top-Of-Stack Cashing):
-
提出原因:
栈中使用的零地址指令会使实现一个功能导致所需的指令特别多,使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存读/写次数。操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。
-
实现:
将栈顶元素全部都缓存在物理cpu的寄存器中,降低对内存的读写,提高效率
动态链接Dynamic Linking
作用:指向运行时常量池中该栈帧所属方法的引用
当java源代码----->字节码文件:所有的变量和方法引用都作为符号引用保存在常量池中
动态链接就是将这些符号引用转换成调用方法的直接引用(与方法的绑定机制有关)
方法的绑定机制:(主要是分在编译期还是运行期)
-
早期绑定:在编译期可知,运行期不变
-
晚期绑定:在运行期根据实际的类型绑定相关方法
虚方法/非虚方法:
-
虚方法:在编译期无法确定方法,在运行期才能确定(除了下面五种其他都是虚方法)
-
非虚方法:在编译期就确定了具体的调用版本(静态方法、私有方法、fina1方法、实例构造器、父类方法)
-
虚方法表:非虚方法不会在表中,可以避免一直在类的方法元数据中搜索,提高查询效率(在类的链接中的解析环节创建,在类变量初始化完成后,JVM也会把该类的方法表加载完毕)
静态/动态语言:
-
静态语言:在编译时就进行检查判断变量自身的类型信息
-
动态语言:在运行时才进行检查判断变量值的类型信息(变量没有类型信息,变量值才有类型信息)
-
为什么需要运行时常量池?
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
方法返回地址Return Address
作用:存放调用该方法的PC寄存器的值,返回该方法被调用的位置
-
正常退出:调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
-
异常退出:栈帧不会保存异常出口这一信息,返回地址需要通过异常表来确定
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
③一些小面试题
-
栈溢出的情况:(线程请求的栈的深度>JVM允许的最大深度)
-
mian()一直调用main(),无限递归
-
定义大量的本地变量,增大此方法栈中本地变量表的长度调整栈大小就能保证不溢出吗:不能保证
-
-
分配的栈空间越大越好吗:不是,栈是线程私有,会导致线程数变小,降低程序执行效率
-
虚拟机栈会有GC吗:不会,栈只有出栈和入栈,不存在GC
-
方法中定义的局部变量是否线程安全:具体情况具体分析(可能安全也可能不安全:如果变量在线程里消亡了就安全,变量是从外部传递或者又传出去了就不安全)
/**
* 面试题
* 方法中定义局部变量是否线程安全?具体情况具体分析
* 何为线程安全?
* 如果只有一个线程才可以操作此数据,则必是线程安全的
* 如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全
*/
public class StringBuilderTest {
// s1的声明方式是线程安全的
public static void method01() {
// 线程内部创建的,属于局部变量
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是线程不安全的,操作的是共享数据
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同时并发的执行,会出现线程不安全的问题
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// StringBuilder是线程安全的,但是String也可能线程不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
(3)本地方法栈Native
本地方法:用关键字native修饰,主要是指JVM中用c/c++实现的方法。
本地方法栈:就是主要是管理本地方法的调用
线程私有
长度可以固定也可以进行扩展
Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
-
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
-
它甚至可以直接使用本地处理器中的寄存器
-
直接从本地内存的堆中分配任意数量的内存。
并不是所有的JVM都支持本地方法