首先:Java虚拟机遵循冯诺依曼计算机模型

运行时数据区

官方地址
image

1. 运行时数据区

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。
其中一些数据区域是在Java虚拟机启动时创建的(方法区、堆),仅在Java虚拟机退出时才被销毁。
其他数据区域是每个线程的(虚拟机栈、本地方法栈、程序计数器)。在创建线程时创建每线程数据区域,在线程退出时销毁每个数据区域。

1.1:方法区

Java虚拟机具有一个在所有Java虚拟机线程之间共享的方法区

该方法区域类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法

方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但是简单的实现可以选择不进行垃圾回收或压缩。该规范没有规定方法区域的位置或用于管理已编译代码的策略。

方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。方法区域的内存不必是连续的。

说明:

  • 方法区包含常量池,在前言【1.2.2:.class文件】常量池,里面存储的是符号引用,当程序运行时,就会转换成直接引用,存储在运行时常量池中,即运行时常量池属于方法区
  • 方法区中存放的是类型信息、常量、静态变量、即时编译器编译后的代码缓存、域信息、方法信息等。随着JDK的发展,方法区中存放的内容也在发生变化。并不绝对

补充:non-final的类变量与final的类变量初始化的时间:

  1. non-final的类变量:就是static的成员变量。

non-final的类变量在类加载的第二个阶段(链接阶段)的准备阶段被赋默认的初始值,然后再类加载的第三个阶段(初始化阶段)被显示初始化(也就是赋值为代码中写的值)。
比如: 定义一个成员变量a, public static int a = 7; a在链接阶段的准备阶段被赋默认值0;然后再初始化阶段被显示初始化为7

  1. final的类变量:就是static final的成员变量。

final的类变量是在编译阶段就被显示初始化了。
比如:定义一个成员变量, public static final int a = 7;a在代码被编译成字节码文件的时候就被赋值为7了。

1.2:堆

Java虚拟机具有一个在所有Java虚拟机线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。

堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会显式释放。Java虚拟机不假定特定类型的自动存储管理系统,并且可以根据实现者的系统要求选择存储管理技术。

堆的大小可以是固定的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以将其收缩。堆的内存不必是连续的。

1.3:Java虚拟机堆栈

每个Java虚拟机线程都有一个私有_Java虚拟机堆栈_,与该线程同时创建。Java虚拟机堆栈存储框架。
public static int b(int i1, int i2) {
    return i1 + i2;
}
public static int a(int i1, int i2) {
    return a(i1, i2);
}
public static void main(String[] args) {
    System.out.println(a(2, 5));
     new Student().hashCode();
}

image

栈帧

  • 栈帧用于存储数据和部分结果,以及执行动态链接,对于方法返回值,并调度异常。
  • 每次调用方法时都会创建一个新栈帧。栈帧的方法调用完成时,无论该完成是正常的还是突然的(它引发未捕获的异常),它都会被销毁。栈帧是从创建栈帧的线程的Java虚拟机堆栈中分配的。每个帧都有其自己的局部变量数组,自己的操作数栈以及对当前方法类的运行时常量池的引用 。
  • 可以使用其他特定于实现的信息(例如调试信息)来扩展栈帧。
  • 局部变量数组和操作数堆栈的大小在编译时确定,并与与帧关联的方法的代码一起提供。因此,帧数据结构的大小仅取决于Java虚拟机的实现,并且可以在方法调用时同时分配用于这些结构的内存。
  • 在给定的控制线程中,只有一帧(用于执行方法的帧)在任何时候都处于活动状态。该帧称为_当前帧_,其方法称为_当前方法_。定义当前方法的_类_是_当前类_。局部变量和操作数堆栈上的操作通常参考当前帧。
  • 如果栈帧的方法调用另一个方法或该栈帧的方法完成,则该框架将不再是当前栈帧。调用方法时,将创建新栈帧,并在控制权转移到新方法时变为新栈帧。在方法返回时,当前栈帧将其方法调用的结果(如果有的话)传递回前一帧。当前一帧变为当前帧时,当前帧将被丢弃。
  • 请注意,由线程创建的栈帧在该线程本地,并且不能被任何其他线程引用。
1.3.1。局部变量数组
  • 每个框架(第2.6节)都包含一个称为其_局部变量的变量数组_。框架的局部变量数组的长度在编译时确定,并以类或接口的二进制表示形式以及与框架关联的方法的代码(第4.7.3节)提供。
  • 单个本地变量可以保存类型的值booleanbytecharshortintfloatreference,或returnAddress。一对局部变量可以包含类型long或的值double
  • 局部变量通过索引解决。第一个局部变量的索引为零。当且仅当该整数比局部变量数组的大小小零至一之间时,该整数才被视为局部变量数组的索引。
  • type long或type的值double占用两个连续的局部变量。只能使用较小的索引来解决该值。例如,double存储在索引为_n_的局部变量数组中的类型值 实际上占据了索引为_n_和 n +1 的局部变量;但是,无法从索引_n_ +1加载局部变量。它可以存储到。但是,这样做会使局部变量_n_的内容无效。
  • Java虚拟机不需要 _n_为偶数。用直观的术语来说,类型的值longdouble不必在局部变量数组中进行64位对齐。实现者可以使用为值保留的两个局部变量来自由决定表示此类值的适当方式。
  • Java虚拟机使用局部变量在方法调用时传递参数。在类方法调用时,所有参数都从连续变量本地变量_0_开始传递。在调用实例方法时,始终使用局部变量_0_将引用传递给在其上调用实例方法的对象(this使用Java编程语言)。随后将任何参数传递到从局部变量_1_开始的连续局部变量中。
    public static int fun2(int i1, int i2) {
        i1 = 3;
        int i = i1 + i2;
        Object object = new Object();
        return i;
    }
   ------------------------------------------------------------------------------- 
    public static int fun2(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4(i1、i2、i、object), args_size=2
         0: iconst_3        // Push `int` constan 给一个常量赋值为3
         1: istore_0        // Store `int` into local variable  将局部变量存入0
         2: iload_0         // Load int from local variable 从局部变量加载下标0 int
         3: iload_1          // Load int from local variable 从局部变量加载下标1 int
         4: iadd            // Add `int`
         5: istore_2        // Store `int` into local variable  将局部变量存入2
         6: new           #10                 // class java/lang/Object new一个Object
         9: dup             // Duplicate the top operand stack value 复制最高操作数堆栈值
        10: invokespecial #1                  // Method java/lang/Object."<init>":()V  初始化object
        13: astore_3        //Store `reference` into local variable  将引用存储到局部变量下标3中
        14: iload_2         // Load int from local variable 从局部变量加载下标2 int
        15: ireturn         // Return `int` from method 从方法返回int
      LineNumberTable:
        line 14: 0
        line 15: 2
        line 16: 6
        line 17: 14
1.3.2:操作数栈
  • 每个帧(第2.6节)都包含一个后进先出(LIFO)堆栈,称为其_操作数堆栈_。帧的操作数堆栈的最大深度是在编译时确定的,并随同该帧关联的方法的代码一起提供(第4.7.3节)。
  • 在上下文清楚的地方,我们有时将当前帧的操作数堆栈简称为操作数堆栈。
  • 创建包含操作数堆栈的框架时,该操作数堆栈为空。Java虚拟机提供了将局部变量或字段中的常量或值加载到操作数堆栈上的指令。其他Java虚拟机指令从操作数堆栈中获取操作数,对其进行操作,然后将结果压回操作数堆栈。操作数堆栈还用于准备要传递给方法的参数并接收方法结果。
  • 例如,_IADD_ 指令(§ IADD)将两个int值加在一起。它要求int将要相加的值是操作数堆栈的前两个值,并由前面的指令压入该值。这两个int值都从操作数堆栈中弹出。将它们相加,并将它们的总和推回操作数堆栈。子计算可嵌套在操作数堆栈上,从而产生可被包含计算使用的值。
  • 操作数堆栈上的每个条目都可以包含任何Java虚拟机类型的值,包括type long或type 的值 double
  • 操作数堆栈中的值必须以适合其类型的方式进行操作。例如,不可能推入两个int值并随后将它们视为a long或推入两个float值并随后使用_iadd_指令将其_相加_。在少数的Java虚拟机指令(_DUP_指令(§ DUP)和_交换_(§ 交换))在运行时数据区域上将其作为原始值使用,而无需考虑其特定类型;这些指令的定义方式不能用于修改或破坏单个值。通过class文件验证(第4.10节)强制实施对操作数堆栈操作的这些限制。
  • 在任何时间点,操作数堆栈都具有关联的深度,其中一个类型的值longdouble对该深度贡献两个单位,而任何其他类型的值则贡献一个单位。
1.3.3:动态链接
  • 每个框架(§2.6)都包含对当前方法类型的运行时常量池(§2.5.5)的引用,以支持方法代码的_动态链接_。的class方法的文件代码是指要调用的方法和要通过符号引用访问的变量。动态链接将这些符号方法引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置关联的存储结构中的适当偏移量。
  • 方法和变量的这种较晚的绑定使方法使用的其他类中的更改不太可能破坏此代码。
1.3.4:普通方法调用完成
  • 如果该调用不会导致直接从Java虚拟机或执行显式语句导致的异常(第2.10节)引发, 则该方法调用将 _正常完成_。如果当前方法的调用正常完成,则可以将值返回给调用方法。当被调用的方法执行返回指令之一(第2.11.8节)时,就会发生这种情况,对返回指令的选择必须适合于要返回的值的类型(如果有)。 [](https://docs.oracle.com/javas... "2.10。 例外情况")throw[](https://docs.oracle.com/javas... "2.11.8。 方法调用和返回说明")
  • 在这种情况下,使用当前帧(第2.6节)来恢复调用程序的状态,包括其局部变量和操作数堆栈,并适当增加调用程序的程序计数器以跳过方法调用指令。然后,执行通常在调用方法的框架中继续进行,并将返回值(如果有)压入该框架的操作数堆栈。
1.3.5:突然方法调用完成
  • 如果方法内执行Java虚拟机指令导致Java虚拟机抛出异常(第2.10节),并且该方法未处理该异常,则方法调用将 _突然完成_。一个执行_athrow_指令(§ athrow)也导致异常被抛出明确,如果该异常没有被当前的方法抓住了,结果突然的方法调用完成。突然完成的方法调用永远不会向其调用者返回值。

1.4:本机方法堆栈

Java虚拟机的实现可以使用传统的堆栈(俗称“ C堆栈”)来支持native方法(以Java编程语言以外的语言编写的方法)。解释程序的实现也可以使用诸如C之类的语言为Java虚拟机的指令集的解释器的实现使用本机方法栈。无法加载native 方法并且自身不依赖于常规堆栈的Java虚拟机实现不需要提供本机方法栈。如果提供,通常在创建每个线程时为每个线程分配本机方法堆栈。

该规范允许本机方法堆栈具有固定大小,或者根据计算要求动态扩展和收缩。如果本机方法堆栈的大小固定,则在创建每个本机方法堆栈的大小时可以独立选择。

例: 当一个对象调用hashcode()方法,对象的构造方法是Java方法,那么存放在Java虚拟机栈中,hashcode()native方法,存放在本机方法堆栈中

public void a() {
    new Student().hashCode();
}

public native int hashCode();

1.5:程序计数器

Java虚拟机可以一次支持多个执行线程。每个Java虚拟机线程都有其自己的 pc寄存器(程序计数器)。在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。

如果不是 native,则该pc寄存器包含当前正在执行的Java虚拟机指令的地址。

如果线程当前正在执行的方法是native,则Java虚拟机的pc 寄存器值未定义。

Java虚拟机的pc寄存器足够宽,可以returnAddress在特定平台上保存一个或本机指针。


少年卫O6Vz4
1 声望0 粉丝