JVM(三)-- 运行时数据区
运行时数据区
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
进程生命周期
进程说明里面的数据会被当前所有的线程共享,会存在线程安全
Method Area(方法区)
JVM运行时数据区是一种规范,真正的实现在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space
方法区是各个线程共享的内存区域,在虚拟机启动时创建,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。
是各个线程共享的内存区域, 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
常量池和运行时常量池
常量池: 就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量(字符串,基本类型)等信息 ,保存在磁盘上。
运行时常量池:常量池是字节码文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池中,并把里面的符号地址变成真实地址,保存在内存中。
常量池在运行时会被加载到运行时常量池中,这时常量池中的信息还只是常量池中的符号,还没有变成java对象
等到具体执行到引用此符号的那行代码时,将其变为对象。
符号引用:4f62 6a65 6374 ----->loading 二进制流是没有办法被cpu直接解析的
直接应用:0x00ab---0x00ba 物理内存
The Constant Pool:https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.4
The Run-time Constant Pool:https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-2.html#jvms-2.5.5
面试题
String类型的常量到底存储在哪里,String常量池
(1)String常量池Java7之前,是存储在方法区的,Java7之后移到了堆内存
方法区很少发生垃圾回收,而堆内存的垃圾回收就比较频繁了,所以放到堆内存会更容易管理
(2)关于String常量的创建
public class SCPDemo {
public static void main(String[] args) {
String str1 = "snail"; // 这个常量一定会放到字符串常量池中
String str2 = "snail";
String str3 = new String("snail");
// 找字符串常量池中是否有该常量,如果有就直接返回,如果没有再创建
String str4 = str3.intern();
// equals 只会比较值 == 会比较地址
System.out.println(str1.equals(str2)); // true
System.out.println(str1 == str2); // true
System.out.println(str1.equals(str3)); // true
System.out.println(str1 == str3); // false
System.out.println(str1.equals(str4)); // true
System.out.println(str1 == str4); // true
}
}
Heap(堆)
(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
(2)Java对象实例以及数组都在堆上分配
(3)堆内存空间不足时,也会抛出OOM
Java对象内存布局
一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充
方法区引用指向堆
堆指向方法区
Java Virtual Machine Stacks(Java虚拟机栈)
(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
栈存储什么?
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
- 栈操作(Operand Stack):记录出栈、入栈的操作;
![img](https://img2018.cnblogs.com/blog/1841773/201910/1841773-20191028010511276-442781621.png)
栈针内存储:
- 局部变量表
- 操作数栈
- 指向运行时常量池的引用
- 方法返回地址
- 动态链接
代码
void a(){
b();
}
void b(){
c();
}
void c(){
}
压栈出栈
Frame(栈帧)
字节码
public static java.lang.Integer calc(java.lang.Integer, java.lang.Integer);
descriptor: (Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iconst_3 // 将int类型常量3压入[操作数栈]
1: invokestatic #11 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_0 // 将int类型值存入[局部变量0]
5: aload_0 // 从[局部变量0]中装载int类型值入栈
6: invokevirtual #12 // Method java/lang/Integer.intValue:()I
9: aload_1 // 从[局部变量1]中装载int类型值入栈
10: invokevirtual #12 // Method java/lang/Integer.intValue:()I
13: iadd // 将栈顶元素弹出栈,执行int类型的加法,结果入栈
14: invokestatic #11 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: astore_2 // 将栈顶int类型值保存到[局部变量2]中
18: aload_2 // 从[局部变量2]中装载int类型值入栈
19: areturn // 从方法中返回int类型的数据
LineNumberTable:
line 11: 0
line 12: 5
line 13: 18
index为0还是1
对于Java虚拟机栈中的Local Variables,到底是从0开始还是1开始,要看当前方法是static还是实例方法。
The Java Virtual Machine uses local variables to pass parameters on method invocation. On class method invocation, any parameters are passed in consecutive local variables starting from local variable 0.
On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language).
Any parameters are subsequently passed in consecutive local variables starting from local variable 1.
栈引用指向堆
栈+ 堆+ 方法区的交互关系
JAVA虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位、去找到对象所在堆中的具体位置,所以对象的方法方法是取决于虚拟机实现
而定的。
现在主流的方法方式有两种:使用句柄和直接指针
-
通过句柄方式访问对象
- 使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含啥了对象的实例数据和类型数据各自的具体地址信息
- 使用句柄访问的好处是reference中储存的是稳定的对象的句柄地址,当对象被移动(垃圾回收时进行对象移动)时候,只需要更新句柄中的对象实例部分指针,reference本身不用被修改。
那么什么是对象的实例数据和类型数据呢?
-
对象实例数据(堆):对象中各个实例字段的数据
-
对象类型数据(方法区):对象的类型、父类、实现的接口、方法等 静态区(也在方法区中)用来存放静态变量,静态块
什么是句柄?
句柄(Handle)来标示应用程序中不同的对象和同类中不同的实例。也可以理解为它是指向实例对象的指针的指针。
String s;//句柄 String s1=new String();句柄=实例对象 s1 = "a";//通过句柄操作对象
-
通过直接指针访问对象
-
使用直接指针访问,那么Java堆中的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
-
使用直接指针访问的最大好处是 速度更快,它节省一次指针定位时间的开销(节省了句柄访问方法中的句柄池寻找实例池中的寻址时间),由于对象的访问在Java中非常频繁,因此这类开销积少成多时也是一项非常可观的成本。
-
一般的虚拟机都是通过指针直接访问对象而通过句柄的这种方法并不是很常用。
-
其中HotSpot 是使用指针的方式来访问对象: Java 堆中会存放访问类元数据的地址,reference 存储的就直接是对象的地址
-
示例:
a b c是三个整型变量,Student是学生类,变量表中存储着 基本数据类型和引用类型的reference,主要以Student类来说明三者调用关系
- 每个线程都要有一个线程栈,栈内存放栈帧,内部为线程中的变量表,既然是在栈中都要按照先入后出的原则进行入栈和出栈。
- 变量表中student指向堆中的student类实例的地址,同时指向方法区中Student类的类型数据(类型数据存储类信息),内部还有其他引用类型的数据,这里举例String类型的name变量,student实例内部存储name由于是引用类型,也会使用指针,在堆中开辟存储空间存储name的内容存储内容“xiaoming”,实质是String类型,在方法区中也有对应类型指向。
Native Method Stacks(本地方法栈)
The pc Register( PC 寄存器 )
-
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
-
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则这个计数器为空。