Java教程

Java运行时数据区域

本文主要是介绍Java运行时数据区域,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Java运行时数据区分为线程共享区域和线程私有区域。线程共享区域随着虚拟机进程生命周期启动和消亡,线程私有区域依赖用户线程的启动和结束而建立和销毁。线程共享区域包括:方法区和堆;线程私有区域包括:栈、本地方法区和程序计数器。如下图所示:

1 程序计数器

程序计数器是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器,通过改变计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
在任何时间,一个处理器(多核处理器情况指一个内核)只会执行一条线程中的指令,各线程相互不影响,所以每条线程都需要独立的程序计数器来存储。程序计算器是线程私有区域。
如果线程正在执行Java方法,程序计数器中的值为正在执行的虚拟机字节码指令的地址,如果是本地方法(Native)值为空(Undefined)。
程序计数器是《Java虚拟机规范》中唯一不会出现OutOfMemoryError的区域
例如,下图为2个线程t1和t2被用一个CPU(内核)交替执行,当执行到t1的位置1的时候,切换到t2执行;当t2执行到位置3的时候,又切回t1,此时需要有个计数器记录之前t1的下一次执行位置。记录执行位置的内存空间称之为程序计数器,每个线程都需要有自己的程序计数器,它记录的是下一次执行的位置数。

字节码中的程序计数器展示如下:

2 虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

2.1 局部变量表

  • 局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
  • 在Class文件中,方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
  • 该表以变量槽(Variable Slot)为最小单位,一个slot可以存放32位以内的数据,比如:boolean、byte、 char、short、int、float等数据,如果存储long、double类型数据,需要占用2个solt。
  • 虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量。
  • 如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量, 则说明会同时使用第N和N+1两个变量槽。
  • 局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键 字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分 配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。

2.2 操作数栈

  • 操作数栈也常被称为操作栈,它是一个先进后出栈。
  • 操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。
  • 操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为 1,64位数据类型所占的栈容量为2。
  • 方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作 数栈中写入和提取内容,也就是出栈和入栈操作。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,例如iadd指令,不能出现一个long和一个 float使用iadd命令相加的情况。

2.3 动态连接

  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
  • Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

2.4 方法出口

当一个方法开始执行后,只有两种方式退出这个方法。

  • 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”。
  • 另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用throw字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成”。这种方法的返回是不会给它的上层调用者提供任何返回值的。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表 和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指 令后面的一条指令等。

例如

public class Demo1 {
    public int m1(){
        int i = 10;
        int b = 20;
        int c = i + b;

        return c;
    }
}

对应的字节码如下

public com.dvomu.jvm.Demo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         //  从局部变量0中装载引用类型值
         0: aload_0
         // 根据编译时类型来调用实例方法
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         // 从方法中返回int类型的数据
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/dvomu/jvm/Demo1;
            
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         // 将一个8位带符号整数压入栈
         0: bipush        10
         // 将int类型值存入局部变量1
         2: istore_1
         // 将一个8位带符号整数20压入栈
         3: bipush        20
         // 将int类型值存入局部变量2
         5: istore_2
         // 从局部变量1中装载int类型值
         6: iload_1
         //  从局部变量2中装载int类型值
         7: iload_2
         // 执行int类型的加法
         8: iadd
         // 将int类型值存入局部变量3
         9: istore_3
         // 从类中获取静态字段
        10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // 从局部变量3中装载int类型值
        13: iload_3
        // 调度对象的虚方法方法
        14: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        // 方法返回
        17: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 6
        line 9: 10
        line 11: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     i   I
            6      12     2     b   I
           10       8     3     c   I

上面的过程用图文简单描述前几步流程:

注意:局部变量表index从1开始

3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度移除或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

4 Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
需要注意的是,《Java虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以 使用最多的HotSpot虚拟机为例进行讲解。
Java堆是垃圾收集器管理的内存区域,因此它也被称作“GC堆”,这就是我们做JVM调优的重点区域部分。
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展,Java虚拟机将会抛出OutOfMemoryError异常。

以下说明以jdk1.8内存模型为例,不再讨论jdk1.7的perm永久代。

4.1 Young 年轻区(代)

Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到OldGen区。

4.2 OldGen老年区(代)

老年区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在年轻代复制转移一定的次数以后,对象就会被转移到老年区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

4.3 Metaspace 元数据空间

元数据空间所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永 久代最大的区别所在。

由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2 * Survivor
年老代:OldGen

官方说明:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gc-ergonomics.html
关于元空间需要注意的是,元空间会自动扩容,默认情况下不受限制,在实际中,经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

为什么要废弃永久代?
官网给出了解释:http://openjdk.java.net/jeps/122

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not
need to configure the permanent generation (since JRockit does not have a permanent
generation) and are accustomed to not configuring the permanent generation.
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

5 方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、 常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK8之前将HotSpot虚拟机把收集器的分代设计扩展至方法区,所以可以将永久代看做是方法区,JDK8之后废弃永久代,用元空间来代替。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表,用于存放编译器生成的各种字面量与符号应用,这部分内容将在类加载后存放到方法区的运行时常量池。
运行时常量池具备动态性,运行期间也可以将新的常量池放入池中,比如通过String类的intern()方法。
例如
当创建String对象是,会在堆里创建对象。如下图

当调用String的interrn()方法时编译器会将字符串添加到常量池中,并返回该常量池的引用。如下图

当使用String str1 = "xxx"时,会先去常量池找"aaa"是否存在相同的常量,如果存在将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。

参考
《Java Virtual Machine Specification》
《深入理解Java虚拟机》第3版

这篇关于Java运行时数据区域的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!