深入解析Java虚拟机运行时数据区域及其内存管理机制

文章目录

深入解析Java虚拟机运行时数据区域及其内存管理机制

运行时数据区域

Java 虚拟机在执行 Java 程序时,会把它所管理的内存划分成若干个不同的数据区域。这些区域各自有其用途。它们创建和销毁的时间也各不相同。有些区域会随着虚拟机进程的启动而存在。有些区域则依赖用户线程的启动和结束来建立和销毁。

1. 程序计数器

Java 虚拟机能够支持多个线程同时进行执行。每个线程都拥有属于自己的程序计数器。在特定的时刻,每个线程都在执行某一个单一方法的代码,也就是该线程当前正在执行的方法。倘若这个方法不是 native 方法,那么程序计数器当中就会包含当前正在被执行的 JVM 指令的地址。要是这个方法是 native 方法,那么计数器的值就是未定义的。JVM 的程序计数器具有足够的宽度。它能够在特定的平台上保存返回地址。同时,也能够保存本地指针。

程序计数器是一块内存空间,它比较小。它可被看作是当前线程所执行字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时,会通过改变这个计数器的值,来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。

Java 虚拟机的多线程是靠线程轮流切换且分配处理器执行时间来达成的。在任何一个特定的时刻,一个处理器(对于多核处理器而言是一个内核)只会去执行一条线程里的指令。为了线程切换后能恢复到正确的执行位置,所以每条线程都需要有一个独立的程序计数器。各条线程之间的计数器互不影响,并且是独立存储的。我们把这类内存区域称作“线程私有”的内存。

如果线程正在执行的是 Java 方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,那么这个计数器的值为空(Undefined)。

此内存区域是唯一一个在 Java 虚拟机规范里不存在规定任何 OutOfMemoryError 情况的区域。

2. 虚拟机栈

虚拟机栈的内存不需要是连续的。

Java®虚拟机规范的第一版里,Java 虚拟机栈被称作 Java 栈。

此规范准许虚拟机栈具备固定的大小,同时也能依据计算需求进行动态的扩展与收缩。倘若虚拟机栈的大小是固定不变的,那么在创建栈的时候,就能够单独去选定每个虚拟机栈的大小。

虚拟机实现能够为程序员或用户提供对虚拟机栈初始大小的掌控。同时,在虚拟机栈进行动态扩展或收缩时,还能对其最大和最小大小进行控制。

以下异常情况与虚拟机栈有关:

虚拟机栈像程序计数器一样是线程私有的,其生命周期与线程相同。它所描述的是 Java 方法执行的内存模型:每个方法在执行时会创建一个栈桢,此栈桢用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从开始调用到最终执行完成的过程,对应着一个栈桢在虚拟机栈中从入栈到出栈的过程。

很多人会将 Java 内存区分为堆内存(Heap)和栈内存(Stack)。这种划分方式较为粗略,实际上的划分要比这复杂得多。这种划分方式仅仅能够表明,对于大多数程序员来说,与对象内存分配关系最为密切且他们最为关注的内存区域就是这两块。

局部变量表

这里存储了编译期能够知晓的各种基本数据类型,包括 boolean 类型、byte 类型、short 类型、char 类型、int 类型、long 类型、float 类型、double 类型;还存储了对象引用,属于 reference 类型;同时也存储了 returnAddress 类型,它指向一条字节码指令的地址。

reference类型:它和对象本身不一样。它或许是一个指向对象起始地址的引用指针,或许是指向一个代表对象的句柄,又或许是指向其他与这个对象相关的位置。

64 位长度的 long 类型数据和 double 类型数据会占用 2 个局部变量空间(Slot)。其余的数据类型仅占用 1 个局部变量空间(Slot)。局部变量表所需的内存空间在编译期就完成了分配。当进入一个方法时,这个方法在帧中需要分配的局部变量空间大小是确定的。并且在方法运行期间,局部变量表的大小不会发生改变。

这个区域有两种异常状况:

当前大部分的 JVM 具备动态扩展的能力。同时,JVM 规范允许存在固定长度的虚拟机栈。

3. 本地方法栈

Java 虚拟机的实现能够借助传统的栈(常被称作“C 栈”)来对本机方法(由 Java 编程语言之外的语言所编写的方法)予以支持。Native 方法栈还可以被 Java 虚拟机指令集的解释器在 C 等语言中进行实现并使用。那些无法加载 native 方法并且自身并不依赖于传统栈的 Java 虚拟机实现,是不需要提供 native 方法栈的。如果有提供的话,通常是在创建每个线程的时候,会为每个线程分配 native 方法栈。

此规范准许 native 方法栈具备固定的大小,亦能依据计算的需求而进行动态的扩展与收缩。倘若 native 方法栈的大小是固定的,那么就能够在创建该栈的时候,单独地为每个 native 方法栈选择其大小。

Java 虚拟机实现能够为程序员或用户提供相关控制。一是可以控制 native 方法栈的初始大小。二是在 native 方法栈大小存在差异的情况下,能够对最大方法栈大小进行控制。三是能够对最小方法栈大小进行控制。

以下异常情况与native方法堆栈有关:

本地方法栈和虚拟机栈发挥的作用很相似。它们的区别在于,虚拟机栈是为虚拟机执行 Java 方法而服务的,本地方法栈则是为虚拟机使用的 native 方法服务的。

虚拟机规范对于本地方法栈中方法的使用语言、使用方式以及数据结构都没有进行强制规定。所以,具体的虚拟机能够自由地对其进行实现。并且,有的虚拟机(例如 HotSpot)直接将本地方法栈和虚拟机栈合并为一个。

4. 堆

Java 虚拟机拥有一个堆,这个堆在所有的 Java 虚拟机线程当中是被共享的。堆属于运行时的数据区域,从堆当中会为所有的类实例以及数组分配内存。

堆在虚拟机启动时被创建。自动存储管理系统(即垃圾收集器)会回收对象的堆存储;对象不会被明确地释放。Java 虚拟机不规定特定类型的自动存储管理系统,可依据实现者的系统需求选择存储管理技术。堆的大小可以是固定的,也能根据计算需求进行扩展或收缩,若不需要更大的堆,便可进行收缩操作。堆的内存不需要是连续的。

Java 虚拟机实现能够给程序员或用户提供对堆初始大小的掌控。同时,如果堆能够动态地进行扩展或者收缩,那么还可以对最大堆大小和最小堆大小进行控制。

以下异常情况与堆有关:

如果计算所需的堆空间大小超出了自动存储管理系统所拥有的堆,那么 Java 虚拟机就会抛出 OutOfMemoryError。

大多数应用中,Java 堆是 Java 虚拟机所管理内存中最大的一块。Java 堆是被所有线程共同共享的一块内存区域,并且在虚拟机启动的时候被创建。这个内存区域存在的唯一目的就是存放对象实例,几乎所有的对象实例都要在这个区域分配内存。Java 虚拟机规范中描述了这一点:所有的对象实例以及数组都需在堆上分配。然而,随着 JIT 编译器的发展以及逃逸分析技术逐渐成熟,栈上分配和标量替换优化技术会引发一些微妙的变化,使得所有对象都分配在堆上的情况渐渐不再那么绝对。

Java 堆是垃圾收集器主要管理的区域。很多时候它也被称作 GC 堆。从内存回收角度来看,因为现在的收集器基本都采用分代收集算法,所以 Java 堆可以细分,包括新生代和老年代。再细致些的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配角度来看,在 Java 堆中,线程是共享的,而这里可能划分出多个分配缓冲区,这些分配缓冲区是线程私有的,被称为 ThreadLocal Allocation Buffer(TLAB)。

无论怎样划分,都和存放内容没有关系。不管是哪个区域,存储的都是对象实例。进一步划分的目的,一是为了能更好地回收内存,二是为了可以更快地分配内存。

Java 虚拟机规范规定,java 堆在物理上可以是不连续的内存空间,只要在逻辑上是连续的就行。在实现的过程中,既能够实现为固定大小的,也能够实现为可扩展的。然而,当前主流的虚拟机都是按照可扩展的方式来实现的,并且可以通过 -Xmx 和 -Xms 来进行控制。如果堆中不存在能够完成实例分配的内存,同时堆也不能够再进行扩展,那么就会抛出 OutOfMemoryError 异常。

5. 方法区

Java 虚拟机有一个方法区域,这个方法区域在所有 Java 虚拟机线程之间是共享的。方法区类似于传统语言编译代码的存储区,也类似于操作系统进程中的“文本”段。它会存储每个类的结构,像运行时常量池、字段和方法数据等,同时还会存储方法和构造函数的代码,其中包括在类和实例初始化以及接口初始化中使用的特殊方法。

方法区域的内存不需要是连续的。

Java 虚拟机实现能够为程序员或用户提供对方法区域初始大小的掌控。同时,在方法区域大小发生变化时,也能够对最大和最小方法区域大小进行控制。

以下异常情况与方法区域有关:

方法区如同 Java 堆一样,是各个线程都能共享的内存区域。它的作用是存储已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。Java 虚拟机规范将方法区描述为堆的一个逻辑部分,并且它还有一个别名叫做 Non-Heap(非堆)。

在 HotSpot 中,许多人更倾向于将方法区称作永久代,即 Permanent Generation。从本质上来看,二者并不等同。这仅仅是因为 HotSpot 虚拟机把 GC 分代收集延伸到了方法区,或者说是通过使用永久代来实现方法区罢了。HotSpot 的垃圾收集器能够像管理 Java 堆那样去管理这部分内存,这样就可以省去专门为方法区编写内存管理代码的工作。

原则上,方法区的实现方式属于虚拟机的实现细节,不受虚拟机规范的约束。使用永久代来实现方法区,现在看来不是一个好的选择,因为这样更容易遭遇内存溢出问题,而永久代有-XX:MaxPermSize 的上限。

Java 虚拟机规范对方法区的限制较为宽松。它不像 Java 堆那样需要连续的内存,也可以选择固定大小或者可扩展。并且,还可以选择不进行垃圾收集。相对来说,垃圾收集行为在这个区域出现得比较少,但数据进入方法区后,并非就像永久代的名字那样永久存在。这个区域的内存回收目标主要包括常量池的回收以及对类型的卸载。

如果方法区不能满足内存分配的需求,那么就会抛出 OutOfMemoryError 异常。 方法区无法满足内存分配需求时,会导致抛出 OutOfMemoryError 异常。 当内存分配需求在方法区得不到满足时,就会有 OutOfMemoryError 异常被抛出。

运行时常量池

运行时常量池是 class 文件中 constant_pool 表对于每个类或每个接口的运行时体现。它包含多种常量,其中有从编译时就已知的数字文字,还有必须在运行时进行解析的方法和字段引用。运行时常量池的功能与传统编程语言的符号表相似,不过它所包含的数据范围比典型的符号表更广泛。

每个运行时常量池是从 Java 虚拟机的方法区域进行分配的。类的运行时常量池是在 Java 虚拟机创建类的时候构建的。接口的运行时常量池是在 Java 虚拟机创建接口的时候构建的。

以下异常情况与类或接口的运行时常量池的构造有关:

运行时常量池属于方法区。在 Class 文件中,除了包含类的版本、字段、方法、接口等相关描述信息外,还有一项重要信息是常量池,此常量池用于存放编译期生成的各种字面量以及符号引用。类加载之后,这部分内容会进入方法区的运行时常量池中进行存放。

直接内存

直接内存并非虚拟机运行时数据区的组成部分,也不属于 Java 虚拟机规范所定义的内存区域。然而,这部分内存被频繁使用,并且有可能引发 OutOfMemoryError 异常。

NIO 引入了一种特定的 IO 方式,这种方式基于通道与缓冲区。它具备这样的能力,即能够利用 native 函数去直接分配堆外内存。之后,通过一个存储在 Java 堆中的 DirectByteBuffer 对象,将其作为这块内存的引用,以此来进行相关操作。在一些特定的场景中,这样做能够显著提升性能,原因在于它避免了在 Java 堆与 Native 堆之间反复进行数据的复制。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

25阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

27阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

23阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读