Java虚拟机02-JVM运行时数据区
1 JVM运行时数据区
JVM运行时数据区(JVM Runtime Area)其实就是指JVM在运行期间,其对计算机内存空间的划分和分配。
image2 程序计数器
是一块较小的内存空间,存储下一条需要执行的Java字节码指令的地址(字节码指令存储在方法区中)。JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只能执行一条线程中的指令。因此每条线程需要一个独立的程序计数器,在能保证线程切换后能恢复到正确的执行位置。
字节码指令
在启动的一个Java进程时,JVM会将class文件数据加载到方法区中。方法区中存储Class文件数据其中包含的类信息和Java字节码指令。
源码
public class ClassStructureMethod {
public void greeting() throws Exception {
try {
int a=1;
int b=1;
int c=a+b;
System.out.println(c);
}catch (Exception e){
System.out.println("catch");
}finally {
System.out.println("finally");
}
}
}
greeting方法Code属性中字节码指令
...省略
public void greeting() throws java.lang.Exception;
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #4 // String finally
20: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: goto 59
26: astore_1
27: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
30: ldc #7 // String catch
32: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
38: ldc #4 // String finally
40: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: goto 59
46: astore 4
48: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
51: ldc #4 // String finally
53: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
56: aload 4
58: athrow
59: return
3 方法区
- 方法区主要用来存储加载的Class文件信息,其中包括了常量池,类信息,字段信息,方法信息(字节码指令),"指向类加载器的引言","指向Class的引言","方法表"。
-
此区域的内存回收目标主要是针对无用类做卸载,一般来说,回收效果难以令人满意,尤其是类型的卸载,条件相对苛刻,但是这部分区域回收是有必要的。
-
当方法无法满足内存需求时,将会抛出OutOfMemoryError异常
4 Java虚拟机栈
-
Java虚拟机栈是一个后入先出栈。每一个线程创建时,JVM会为这个线程创建一个私有虚拟机栈。
-
当线程调用某个对象的方法时,JVM会相应地创建一个栈帧压入虚拟机栈中,返回时从虚拟机栈中取出。线程方法的调用返回对应着一个栈帧在虚拟机栈中的入栈和出栈的过程。
-
当前虚拟机正在执行的方法的栈帧被称为"当前活动的栈帧",永远位于虚拟机栈顶部。
4.1 虚拟机栈中的方法调用
案例
public class Demo3 {
public void test1() {
System.out.println("我是test1的方法");
test2();
}
public void test2() {
System.out.println("我是test2的方法");
test3();
}
public void test3() {
System.out.println("我是test3的方法");
}
public static void main(String[] args) {
Demo3 demo3=new Demo3();
demo3.test1();
}
}
虚拟机栈中的流程
虚拟机栈执行过程.jpg4.2 栈帧结构
image4.2.1 局部变量表
-
局部变量表(Local Variable Table)是一组变量值存储空间(类似于数组),用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Slot)为最小单位,如果局部变量表类似于数组,那么变量槽(Slot)就相当于数组中一个数据块的大小。
-
局部变量表中变量最大数量在将源码文件编译成Class文件时就以确定,存储在方法的Code属性locals中。
-
需要注意的是如果是实例方法(非static的方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用。
局部变量表存储数据类型
-
八大数据类型(boolean、byte、char、short、int、float、long、double)
-
对象引用
-
returnAddress类型(指向了一条字节码指令的地址)
存储数据方式
-
32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。对于long、double这种64位数据类型 值需要用两个(Slot)来存储.
-
在局部变量表中,boolean、byte、char、short引用类型这五种类型,在虚拟机栈中空间和int是一样的都占用8个字节,而long、double占用16个字节。
4.2.2 操作数栈
操作数栈是用来存储Java字节码指令计算过程中的参数和结果的数据结构。Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是-操作数栈。
栈的深度已经在将源码文件编译成Class文件时就以确定,存储在方法的Code属性stack中。
操作数栈的存储
操作数栈也是被组织成一个以字长为单位的数组,不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的。对于非long、double都使用8个字节存储占用1个变量槽,对于long、double都使用16个字节存储占用2个变量槽,这也应证了Java虚拟机对数据的操作就是在操作数栈和局部变量表中相互传递。
操作数栈的案例
begin
iload_0 // 将第一个int型本地变量推送至栈顶
iload_1 // 将第二个int型本地变量推送至栈顶
iadd // 将栈顶两int型数值相加并将结果压入栈顶
istore_2 // 将栈顶int型数值存入第三个本地变量
end
前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。
image4.2.3 方法返回地址
方法返回在虚拟机中流程
-
恢复上层方法的局部变量表和操作数栈
-
把返回值压入调用者调用者栈帧的操作数栈
-
调整 PC 计数器的值以指向方法调用指令后面的一条指令
方法的正常返回
当执行遇到返回指令return,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
方法的异常返回
当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
4.2.4 动态链接
在了解动态链接之前我们需要了解如下几个概念,“符号引用”,“直接引用”,“静态链接”
符号引用
在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。符号引用是方法区中常量池中一类常量。符号引用用来描述类中,类,接口,字段,方法的描述信息。
对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。
解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
#define CONSTANT_Class 1 //对一个类或接口的符号引用
#define CONSTANT_Fieldref 2 //对一个字段的符号引用
#define CONSTANT_Methodref 3 //对一个类中方法的符号引用
#define CONSTANT_InterfaceMethodref 4 //对一个接口中方法的符号引用
- 对于一个类或接口的描述信息包括类或接口的全限定名称
#8 = Class #39 // jvm/ClassStructureMethod
- 对于一个字段描述信息包括字段所在的类以及描述符,描述符包括字段的名称和类型
#2 = Fieldref #3.#19 // jvm/TestClass.m:I
- 对于一个方法描述信息包括方法所在的类以及描述符,描述符包括方法的名称,参数和返回类型
#1 = Methodref #9.#30 // java/lang/Object."<init>":()V
因此符号引用以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
直接引用
直接引用可以是直接指向目标在内存中的指针,其值中存储目标在内存中的地址。这里所谓的目标就是指符号引用表示的类,接口,字段,方法。也就是说我们程序执行中需要的类在没有加载到内存中时可以使用符号引用来表示。
当我们将一个类加载Class文件加载到Java虚拟机的内存中。在解析时会将符号引用转换为直接引用。
静态链接
静态链接就是在类加载阶段将符号引用转换为直接引用的过程。
在Java中静态链接就是类加载机制中的一个过程—解析,将class文件中的一部分符号引用直接解析为直接引用的过程。同时我们也称它为“解析调用”
由于Java中多态的特性,并不所有的符号引用都能在类加载阶段转换为直接引用。因此静态链接需要如下条件。
-
方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。可以概括为:编译期可知、运行期不可变。
-
其中invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法
动态链接
动态链接就是在运行期间将符号引用转换为直接引用的过程。
Java中天生可以动态扩展的语言特性就是依赖动态加载和动态链接这个特点实现的。
动态扩展就是在运行期可以动态修改字节码,也就是反射机制与cglib
5 本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用类似,它们之间的区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
6 堆
对于大多数应用程序而言,Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java虚拟机规范中描述道:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都在堆上分配的定论也并不“绝对”了。
Java堆与垃圾回收器
Java堆是垃圾回收器管理的主要区域,因此被称为“GC堆”(Garbage Collected Heap)。
从内存回收角度看
由于目前收集器基本采用分代收集算法,所以Java堆可细分为:新生代和老年代。
从内存分配角度来看
由于堆是线程共享的必然会存在并发的问题,JVM为每一个线程固定了一个存储区域线程私有的分配缓冲区(TLAB:Thread Local Allocation Buffer)。
7 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致OutOfMemoryError异常出现。
直接内存一般适用适用NIO 适用堆外内存的情况