likes
comments
collection
share

JVM-运行时数据区

作者站长头像
站长
· 阅读数 15

本文是JVM系列第三篇 基于Java虚拟机规范(Java Virtual Machine Specification)定义,将运行时数据区定义如下:

  1. 程序计数器(Program Counter Register):程序计数器是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机中,每个线程都有一个程序计数器。
  2. Java虚拟机栈(Java Virtual Machine Stacks):每个线程在创建时都会创建一个Java虚拟机栈,用于存储方法的局部变量、操作数栈、动态链接、方法出口等信息。每个方法在执行的同时都会创建一个栈帧用于存储这些信息,栈帧在方法执行完毕后被销毁。
  3. 本地方法栈(Native Method Stacks):本地方法栈与Java虚拟机栈类似,但是它为执行本地方法(Native Method)服务。Java虚拟机栈和本地方法栈的区别在于,Java虚拟机栈为Java方法服务,而本地方法栈为本地方法服务。
  4. Java堆(Java Heap):Java堆是Java虚拟机中最大的一块内存区域,也是被所有线程所共享的一块内存区域。Java堆用于存储对象实例和数组,是垃圾收集器管理的主要区域。
  5. 方法区(Method Area):方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法区也是被所有线程所共享的一块内存区域。
  6. 运行时常量池(Runtime Constant Pool):运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,以及运行时生成的一些常量。
  7. 直接内存(Direct Memory):直接内存并不是Java虚拟机运行时数据区的一部分,但是它也是与Java堆密切相关的一块内存区域。直接内存是通过使用Native函数库直接分配堆外内存的方式来提高内存使用效率。

运行时数据区的划分也随着JDK的发展不断变迁,如下图所示

JVM-运行时数据区 在JDK 1.8 中加入了元数据区的概念,将原来保存在方法区中的运行时常量池和类常量池都包括其中,下面基于这个版本讲述运行时区域

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)是Java虚拟机运行时数据区之一,用于存储方法的局部变量、操作数栈、动态链接、方法出口等信息。每个线程在创建时都会创建一个Java虚拟机栈,Java虚拟机栈与线程同生命周期。

Java虚拟机栈可以看作是Java方法执行的内存模型。每当一个方法被调用时,Java虚拟机就会为该方法创建一个栈帧(Stack Frame),用于存储该方法的局部变量、操作数栈、动态链接、方法出口等信息。当方法执行完毕时,对应的栈帧就会被弹出,销毁。

JVM-运行时数据区 Java虚拟机栈由一系列栈帧(Stack Frame)组成,每个栈帧对应一个方法的执行。栈帧包含了方法的局部变量表、操作数栈、动态链接、方法出口等信息。

局部变量表

它定义为数字数组,主要用于存储方法参数和定义在方体内的局部变量,包含基本数据类型,对象引用,以及returnAddress类型。它建立在线程的栈上,是线程的私有数据,因此不存在数据的安全问题。 局部变量表所需的容量在编译期间确定,在运行期间是不改变其容量

操作数栈

用于存储操作数和中间结果,它是一个后进先出的栈,在方法执行的过程中,根据字节码指令、往栈中写入或取出数据,即入栈/出栈。字节码指令将值压入操作栈,其余的字节码指令将操作数取出栈,进行操作之后再将结果压入栈。操作包括:复制、交换、求和等。

动态链接

动态链接用于指向运行时常量池中该方法所使用的符号引用 在介绍动态链接之前先说说静态链接,即字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期间保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。但是,如果被调用方法在编译期间无法被确定下来,只能在程序运行时将调用方法的符号引用转换为直接引用,由于这种引用转换的过程具备动态性,被称为动态链接。

方法返回地址

方法出口用于指向方法执行完毕后的返回地址。 当一个方法开始执行后,可以通过两种方式退出该方法。 第一种是执行引擎遇到方法返回的字节码指令,此时返回值会传递到上层调用者,这种方式称为正常完成出口。 另外一种退出方式是在方法执行中遇到异常,这个异常在方法体内没有得到处理,就会导致方法退出,这种方式称为异常完成出口。由于是异常退出,就不会给上层调用者任何返回值。 无论采取上面那种退出方式,方法都会到处调用它的位置,程序才能继续执行。方法在返回的时候需要在栈帧中保存一些信息,用来恢复调用该方法的上层方法的执行状态。这里可以通过方法调用者的程序计数器存放返回地址,如果是正常退出方法,上层方法会从程序计数器中保存的地址继续执行接下来的步骤。如果是异常退出的情况,返回地址就需要异常处理器来确定了。

Java虚拟机栈的大小可以通过启动参数-Xss来设置,默认值为1M。如果Java虚拟机栈空间不足,将会抛出StackOverflowError异常;如果Java虚拟机无法为新的栈帧分配空间,将会抛出OutOfMemoryError异常。

程序计数器

程序计数器(Program Counter Register)是Java虚拟机运行时数据区的一部分,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,用于记录当前线程正在执行的字节码的行号。

程序计数器在Java虚拟机中属于线程私有的内存区域,它的作用是记录当前线程执行的字节码指令地址。在Java虚拟机的多线程环境中,程序计数器可以保证线程切换后能够恢复到正确的执行位置。

程序计数器在Java虚拟机中并不是一个内存区域,而是一个寄存器。它的大小与所执行的Java虚拟机实现相关,通常为32位或64位。由于程序计数器只是一个指示器,它不参与方法调用和执行的过程,因此在Java虚拟机规范中并没有规定程序计数器需要遵循特定的内存模型。

程序计数器的主要作用有两个:一是保证线程切换后能够恢复到正确的执行位置;二是为Java虚拟机的字节码执行引擎提供“跳转”、“循环”、“分支”、“异常处理”等功能,使得Java虚拟机能够正确地执行Java程序中的各种控制流语句。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机所使用到的Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

说白了,本地方法(Native Method)就是一个Java调用非Java代码的接口。 当Java应用需要与Java之外的环境交互时就需要使用本地方法,特别与底层系统、操作系统以及硬件打交道时就会用到本地方法。大家可以把本地方法理解为一种交流机制:它提供了一个对外的简洁的接口,让我们无需去了解Java应用之外的细节。

那么JVM是如何使用Native Method的呢?当一个类第一次被使用时,类的字节码会被加载到内存,在字节码的入口维持着该类所有方法描述符的list,包括:方法代码来源,参数,方法描述符(例如:public)等等。

如果方法描述符是native,同时描述符块将有一个指向该方法实现的指针,而具体实现在DLL文件内,此时DLL文件会被操作系统加载到Java程序的地址空间里。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前, DLL才会被加载,即通过调用java.system.loadLibrary()实现的。

Java堆

Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。Java对象实例以及数组都在堆上分配。堆的大小可以是固定的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。Java虚拟机实现可以为程序员或用户提供对堆初始大小的控制,如果可以动态扩展或收缩堆,还可以控制堆的最大和最小大小。

Java堆是垃圾收集器管理的主要区域,所以也被称为GC堆。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代再细分就是:Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不论如何划分,都与存放内容无关,无论哪个区域,存放的都仍然是对象实例;进一步划分的目的是为了更好的回收内存,或者更快地分配内存。

方法区

方法区和堆一样是线程共享的内存区域,它用来存放被虚拟机加载的类型信息、运行时常量池、静态变量、JIT代码缓存、域信息、方法信息等。方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,有如下特点:

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的。
  • 方法区的大小,和堆空间一样,可以选择固定大小和可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机就会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGenspace或者 java.lang.OutOfMemoryError: Metaspace。
  • 关闭JVM就会释放这个区域的内存。

元数据空间

在JDK 8中,新增了一个元数据区(Metaspace)来代替永久代(PermGen),用于存储类的元数据信息。元数据区是Java虚拟机运行时数据区的一部分,主要用于存储类的元数据信息,如类名、方法名、字段名、访问修饰符、注解等。

在永久代中,存储类的元数据信息需要指定初始大小和最大大小,并且会受到垃圾回收器的影响。而在元数据区中,存储类的元数据信息不再受到初始大小和最大大小的限制,而是会根据需要动态地分配和回收内存空间。此外,元数据区也不再使用垃圾回收器进行垃圾回收,而是使用与Java堆相同的垃圾回收机制。

元数据区的优点在于,它可以避免永久代中出现的类加载器泄露和元数据区满导致的OutOfMemoryError等问题。同时,元数据区的动态分配和回收机制也可以更好地适应不同的应用场景和使用需求。

元数据区的大小可以通过启动参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize来设置,如果元数据区空间不足,将会抛出OutOfMemoryError异常。

堆、方法区和虚拟机栈的关系整理

JVM-运行时数据区

在右边创建了AppMain 类,在运行时JVM 会把AppMain的信息放入到方法区,因为方法区会存放类型信息。同时main 的方法本身也会放入到方法区。接下来的new Sample(“测试1”)的语句中Sample的自定义对象会放到堆里面,而对应的test1 应用会放入到虚拟机栈中,对应的test1.printName()方法的执行会在虚拟机栈中的栈帧中通过指令执行完成。另外下面的class Sample也是放到方法区中的,声明的private name,其中name的引用放在虚拟机栈中,name对应的对象放在堆中。对应的printName方法是放在方法区中的。

运行时常量池

运行时常量池(Runtime Constant Pool)是Java虚拟机运行时数据区的一部分,用于存储编译期生成的各种字面量和符号引用。在Java源代码编译为字节码文件时,其中包含了一些常量,如字符串、数字、类和方法的符号引用等,这些常量会被编译器放到运行时常量池中。

运行时常量池中存储的常量包括两种类型:字面量和符号引用。字面量常量包括字符串、数字、布尔值、字符等;符号引用常量包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。

运行时常量池的大小是在类加载时确定的,并且可以通过启动参数进行设置。在运行时,Java虚拟机需要使用运行时常量池中的常量来执行字节码指令,如字符串拼接、方法调用等。如果常量池空间不足,将会抛出OutOfMemoryError异常。

在JDK 8及以后的版本中,运行时常量池(Runtime Constant Pool)被放置在Java堆(Heap)中。在JDK 7及以前的版本中,运行时常量池被放置在永久代(PermGen)中。

直接内存

直接内存(Direct Memory)是Java虚拟机运行时数据区的一部分,与Java堆和方法区等一起构成了Java虚拟机的内存模型。直接内存通常是通过Java NIO库中的ByteBuffer.allocateDirect()方法来创建的。

直接内存与Java堆上的对象不同,它直接使用操作系统的内存空间,而不是受限于Java堆的大小。因此,直接内存可以避免在Java堆和操作系统之间进行频繁的数据传输,从而提高了程序的性能。

直接内存的使用方式类似于Java堆中的内存,可以通过调用ByteBuffer的get()和put()方法来读写数据。在使用完毕后,可以通过调用ByteBuffer的clear()方法来释放直接内存,或者通过调用System.gc()方法来触发垃圾回收器回收直接内存。

需要注意的是,直接内存的分配和释放不由Java虚拟机来管理,而是由操作系统来管理。因此,在创建大量直接内存时,需要注意避免出现OutOfMemoryError异常和操作系统资源的耗尽等问题。此外,直接内存也会受到操作系统的限制,例如单个进程可以使用的直接内存大小等。

参考: 1.17张图带你了解,JVM 运行时数据区