Java虚拟机的运行时数据区域

本文详细解析Java虚拟机(JVM)的内存布局,涵盖线程私有与共享区域,如程序计数器、虚拟机栈、本地方法栈、堆、方法区、运行时常量池及直接内存。探讨各区域的功能、作用及异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 前言

Java虚拟机在执行java程序的过程中将他所管理的内存划分为不同的内存区域。这些区域有着各自的用途,不同的创建及销毁时间。

Java虚拟机管理的内存包括了如下几个区域:

  • 方法区(Method Area)
  • 虚拟机区(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 堆(Heap)
  • 程序计数器(Program Counter Register)

此处先说明各类概念:

线程私有:顾名思义既是被某线程单独占有,不在各线程之间进行共享的区域。该类区域的生命周期与线程相同。

二 运行时区域

2.1 程序计数器(Program Counter Register)

  • 占用空间小,线程私有

程序计数器(Program Counter Register)是一块较小的内存空间。各线程之间计数器独立存储。我们称这类区域为“线程私有”的区域。

  • 相当行号指示器

主要作用就是标识当前线程执行的字节码的行号,在每一条线程的内部,字节码解释器通过改变程序计数器的值来选取及记录下一条需要执行的字节码指令,可以看成是当前线程所执行的字节码的行号指示器。

  • 保证线程切换后能够正确执行

Java虚拟机多线程的实现是通过轮流切换并分配时间片给每个线程来完成。每个CPU线程在一个确定的时刻只会执行一条线程中的指令,程序计数器可使得当前线程在切换线程后仍然能够恢复到正确的执行位置,此处即使上述所说的记录的作用。

  • Native方法计数器为空

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

2.2 Java虚拟机栈(JVM Stack)

  • 线程私有

意味着每条线程都会有相应的Java虚拟机栈

  • 方法执行的内存模型

虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用来存储局部变量表,操作数栈,动态链接,方法出口等信息。

每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 存放局部变量表(编译期确定)

局部变量表存放着**编译器可知的六种数据类型,对象引用以及returnAddress类型(指向了一条字节码指令的地址)。**其中长度64的long和double类型会占用两个局部变量空间(Slot),其余的数据类型只占用一个。

  • 栈溢出与内存溢出

如果线程请求的栈深度大于JVM所允许的深度,将抛出StackOverflow异常。比如递归调用多次时便会创建多个栈帧直到栈溢出;

如果Java虚拟机可以动态拓展(大部分JVM都可以动态拓展,但Java规范中也允许固定长度的虚拟机栈),如果拓展时无法申请到足够的内存,便会抛出OutOfMemoryError异常。

2.3 本地方法栈(Native Method Stack)

  • 与Java虚拟机栈(JVM Stack)的区别

两者所发挥的作用是十分相似的,之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

  • 虚拟机规范没有强制规定本地方法栈中方法的实现

在虚拟机规范没有强制规定本地方法栈中方法使用的语言,使用方式以及数据结构。因此具体的虚拟机可以自由地实现它。

  • 栈溢出与内存溢出

与Java虚拟机栈一样

2.4 Java堆(Java Heap)

  • 占用空间大,线程共享

是JVM所管理的内存中最大的一块。并且为所有线程共享

JVM规范规定:Java Heap可以处于物理上不连续的内存空间中,只要逻辑是连续的即可。

  • 存放对象实例

这是Java Heap存在的唯一目的。几乎所有的对象实例都在这里分配内存。

JVM规范中描述:所有的对象实例以及数组都要在Heap上分配。

  • 垃圾收集器管理的主要区域

Java Heap是垃圾收集器管理的主要区域,也被称为GC堆(Garbage Collected Heap)。

从垃圾回收的角度看,java Heap可以细分为:新生代和老年代。

再细致点可以分为:Eden空间,From Survivor空间,To Survivor空间等

从内存分配的角度看,线程共享的Java Heap中可能划分出多个线程私有的分配缓冲区。

  • 内存溢出

如果在Heap中没有内存完成实例分配,并且Heap也无法再拓展时,将会抛出OutOfMemoryError异常。

2.5 方法区(Method Area)

  • 线程共享
  • 存放数据

它用于存储已被虚拟机加载的类信息,常量,静态变量,即编译器编译后的代码等数据。

JVM规范描述:方法区为堆的一个逻辑部分

  • JVM规范限制宽松

除了跟Java Heap一样不需要连续的内存和可以选择固定大小或者可拓展外,还可以选择不实现垃圾收集。相对而言,垃圾回收在这个区域是比较少出现的。像HotSpot虚拟机就使用永久代来实现方法区,但这样更容易遇到内存泄露。

  • 难以回收

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

这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但该区域的回收效果较差,尤其是类型的卸载条件相当严苛。但不完全回收又会造成内存泄漏。

2.6 运行时常量(Runtime Constant Pool)

  • 方法区的一部分

Runtime Constant Pool是方法区(Method Area)的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  • JVM规范限制宽松

JVM对于Class文件的每一部分格式都有严格规定,但对于运行时常量池,JVM规范没有做任何细节要求。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在在Runtime Constant Pool中。

2.7 直接内存(Direct Memory)

  • 定位

Direct Memory并不是JVM运行时数据区的一部分,也不是JVM规范的一部分,但这部分区域也被频繁使用。

NIO类中引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java Heap中的Direct Byte Buffer对象作为这块内存的引用进行操作,这样能够避免在Java Heap和Native Heap中来回复制数据。

三 引申

3.1 局部变量表(Local Variable Table)

局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在编译期便能够确定对应方法所需要分配的局部变量表的最大容量。

局部变量表的容量最小单位为一个变量槽(Variable Slot),即上文所说的Slot。在JVM规范中并没有明确指明一个Slot应占用的内存空间大小,只是提到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference 或 returnAddress 类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放。(此处所提到的returnAddress类型已经很少见了,在古老的虚拟机中曾经用其来实现异常处理,现在已经用异常表代替。)但这种描述与明确指出 “每个Slot占用32位长度的内存空间” 是有一些差别的,它允许 Slot 的长度可以随着处理器、操作系统或虚拟机的不同而发送变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个 Slot,虚拟机仍要使用对齐和补白的手段让 Slot 在外观上看起来与32位虚拟机中的一致。

Java语言中明确指出需要使用64位的数据类型只有double与long(Java 虚拟机规范中没有明确规定 reference 类型的长度,它的长度与实际使用32还是64位虚拟机有关,如果是64位虚拟机,还与是否开启某些对象指针压缩的优化有关)。对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始至局部变量表最大的 Slot 数量。如果访问的是 32 位数据类型的变量,索引 n 就代表了使用第 n 个 Slot,如果是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。对于两个相邻的共同存放一个 64 位数据的两个 Slot,不允许采用任何方式单独访问其中的某一个,Java 虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。

在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非 static 的方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。

为了尽可能节省栈帧空间,局部变量中的Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot 的复用会直接影响到系统的垃圾收集行为:若没有将某局部字段设置为null,则GC Root会仍然保持与该Slot的关联。

四 总结

JVM虚拟机中

线程私有的有Program Counter Register, JVM Stack, Native Method Stack

线程共享的有Java Heap, Runtime Constant Pool, Direct Memory。

大致上程序执行时对各个区域的利用:

执行程序开始时,线程会将编译器编译后的字节码等信息存储在Method Area中,程序运行期间会经常从此处读取数据用以程序的执行。

每个字节码程序在由线程运行时,每个线程都会自带一个线程私有Program Counter Register以用来读取与记录字节码执行的当前位置,使得线程可以有条不紊地执行字节码,同时防止因为上下文切换而导致无法完整执行字节码。

面向对象程序的执行需要实例化出一个个的对象或数组,而这些对象与数组便存放在内存区域最大的Java Heap,根据垃圾回收的时间周期可以简单分为新生代与老年代。而一些诸如int的原始类型则使用栈存储。

在执行一个个方法的同时都会创建一个栈帧(Stack Frame)用来存储局部变量表,操作数栈,动态链接,方法出口等信息。而根据执行的代码是Java或是其他而存储在JVM Stack 或 Native Method Stack。

为了有效减少重复的数据,Runtime Constant Pool存放编译期生成的各种字面量和符号引用,通过指向Runtime Constant Pool中的数据可以有效减少字符串等的实例化损耗。

最后要说的是:在每个虚拟机中他们实现的具体方式有所区别,同时因为虚拟机的版本迭代具体实现也有所不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值