深入理解运行时数据区
前言
首先,要想真正的深入理解运行时数据区模型,可以从官网的虚拟机介绍入手,里面有这么一段话:
The Java Virtual Machine is an abstract computing machine. Like a real computing machine, it has an instruction set and manipulates various memory areas at run time. It is reasonably common to implement a programming language using a virtual machine;
// Java虚拟机是一种抽象的计算机,像一台真正的计算机一样,它有指令集,并在运行时操作各种内存区域.
先前我们理解运行时数据区都是在死记硬背这些区域和它的作用,换种记法,我们可以拿运行时数据区和我们真正的计算机中各区域做比较来进行记忆,或许是个不错的选择哦!大家可以思考下是如何进行比较记忆的,那么接下来一起来学习吧!
运行时数据区【Run-Time Data Areas】总体概述
我们通过官网可以知道,Java虚拟机是一种抽象的计算机,像一台真正的计算机一样。那既然都这么明确的告诉我们了,那么我们可以思考一些问题,JVM是如何进行设计的,是设计跟真实的计算机一样嘛,那肯定是不可能的,而是通过共同的抽象模型来进行设计的,大家都遵循统一的设计理念。这时我们就会想到冯诺依曼计算机体系模型了,计算机是按照这个模型来设计的。
冯诺依曼计算机体系图:
通过冯诺依曼计算机体系模型,知道计算机大体是由输入设备、存储器、运算器、控制器、输出设备组成,而在上一篇文章中,提到了通过前期一系列编译(java文件 --> class文件),JVM通过装载class文件,到JVM内部各种折腾,生成0101机器码给计算机硬件层面,那不正对应着我们计算机体系模型中的输入设备【可以看做是class文件】,输出设备【可以看做是0101机器码】,而运行时数据区的设计又和计算机的另一层面发展可以相关联。
CPU和内存的交互,早期的计算机性能不高的时候,内存和CPU的交互是可以直接进行,但随着科技的不断进步,CPU的发展迅速【摩尔定律】,CPU的性能高,直接交互远远降低了CPU性能,引入高速缓存的概念,而在这过程中又会有缓存一致性问题,通过【MESI,MSI,MOSI】缓存一致性协议去解决
摩尔定律是英特尔创始人之一戈登·摩尔的经验之谈,其核心内容为:集成电路上可以容纳的晶体管数目在大约每经过18个月到24个月便会增加一倍。换言之,处理器的性能大约每两年翻一倍,同时价格下降为之前的一半。
CPU和内存交互图:
CPU和内存交互中,知道了在运行时数据区中,需要有存储数据的地方,在运行时数据区中堆不就是存放数据的地方,在官网的介绍:
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
//堆是Java虚拟机线程所共享的,堆是运行时数据区区域,所有类实例和数组的内存都是在堆中分配的
JDK8_JVM内存模型图如下:
类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间),而这些文件所对应的位置就是运行时数据区,接下来从整体分散讲各个分点。
方法区【Method Area】(非堆)
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads.
The method area is created on virtual machine start-up.
//方法区是各个线程共享的内存区域,在虚拟机启动时创建
It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
//用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
//如果需要分配的大小在内存中方法区空间不足时,虚拟机会抛出OutOfMemoryError异常
在上一篇类加载文章中,可以知道装载的第二步:②表示字节流的静态存储结构转化为方法区运行时数据结构,如下图:
JVM运行时数据区是一种规范,真正的实现
JDK1.8中方法区用的是Metaspace元空间
JDK1.6、1.7中方法区用的是Perm Space永久代
元空间和永久代最大的区别就是元空间不在虚拟机中,使用的是本地内存,而永久代使用的是JVM内存【如果字符串常量池在永久代中,容易出现性能问题和内存溢出】
堆【Heap】
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up.
//Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
//Java对象实例以及数组都在堆上分配
在装载的第三步:在Java堆中生成一个代表这个类的Java.lang.class对象,作为对方法区中这些数据的访问入口,如下图:
虚拟机栈【Java Virtual Machine Stacks】
经过上面的过程,类加载机制的装载过程已经完成,后续的链接、初始化也会相应生效。
假如目前的阶段是初始化完成了,后续还需要做啥,那肯定是use啊,那怎样才能被使用到?里面的内容怎样才能被执行?比如通过主函数main调用其它方法,这种方法实际上时main线程执行之后调用的方法,即要想使用里面的各种内存,得要以线程为单位,执行相应的方法才行,那一个线程执行状态如何维护?一个线程可以执行多少个方法?这样的关系如何维护呢?
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread.
//每一个Java虚拟机线程都有一个私有的虚拟机栈,和线程同时被创建。
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
A Java Virtual Machine stack stores frames (§2.6).
A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes.
//每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
this.show();
return c;
}
public void show() {
System.out.println("hahaha");
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
图解如下:
执行完成后依次弹栈
栈帧的组成
局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中
操作数栈:以压栈和出栈的方式存储操作数的
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
方法出口(方法返回地址):当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
结合字节码指令理解栈帧
javap -c Math.class,以compute()方法为例,需要通过JVM指令手册查找其含义,JVM手册会放入到下载资源里,有需要理解的可自取!
Compiled from "Math.java"
public class com.jiang.jvm.Math {
public static final int initData;
public static com.jiang.jvm.User user;
public com.jiang.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1 //将int类型常量1压入操作数栈
1: istore_1 //将int类型值存入局部变量1
2: iconst_2 //将int类型常量2压入操作数栈
3: istore_2 //将int类型值存入局部变量1
4: iload_1 //从局部变量1中装载int类型值
5: iload_2 //从局部变量2中装载int类型值
6: iadd //执行int类型的加法
7: bipush 10 //常数到操作数栈 10
9: imul // 乘
10: istore_3 //将int类型值存入局部变量3
11: aload_0 //从局部变量0中装载引用类型值
12: invokevirtual #2 // Method show:()V //调度对象的实便方法
15: iload_3 //从局部变量3中装载int类型值
16: ireturn //方法返回
public void show();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hahaha
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: new #6 // class com/jiang/jvm/Math
3: dup
4: invokespecial #7 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #8 // Method compute:()I
12: pop
13: return
static {};
Code:
0: new #9 // class com/jiang/jvm/User
3: dup
4: invokespecial #10 // Method com/jiang/jvm/User."<init>":()V
7: putstatic #11 // Field user:Lcom/jiang/jvm/User;
10: return
}
程序计数器【The pc Register】(PC)
我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine’s pc register is undefined. The Java Virtual Machine’s pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
//如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
//如果正在执行的是Native方法,则这个计数器为空。
本地方法栈【Native Method Stacks】
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行
那如果在Java方法执行的时候调用native的方法呢?