运行时数据区域

Java 虚拟机运行时数据区

程序计数器

  • 当前线程所执行的字节码的行号指示器
  • 线程私有,每条线程拥有一个独立的程序计数器,独立存储
  • 如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为 undefined(这里请看下面详细解释)
  • 此内存区域是唯一一个在《 Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域

Native Method

一个 java 调用非 java 代码的接口,是一个 java 的方法但该方法的实现由非 java 语言实现。

留个坑,找时间具体学习一下

Java 线程执行 native 方法时程序计数器为空,如何确保 native 执行完后的程序执行的位置?

在JVM规范中是这么说的

If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined

可见这里说的是 undefined ,未定义可以为任何值,而不是一定为空

原因是程序计数器存放的是 Java 字节码的地址,而 native 方法的方法体并不是由 Java 字节码构成,所以将程序计数器设定为未定义

native 方法执行完后会退出(栈帧pop),方法退出返回到被调用的地方继续执行程序,调整程序计数器的值以指向方法调用指令后面的一条指令

Java 虚拟机栈

  • 线程私有
  • 描述 Java 方法执行的线程内存模型:每个方法被执行的时候, Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  • 局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、 byte、char、 short、 int、 float、 long、 double)、对象引用(reference类型,它并不等同于 对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)
  • 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 64位长度的 long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小,这里指变量槽的数量不变,但虚拟机真正使用多大的内存空间(譬如 1个变量槽占用 32个比特、64个比特或者更多)来实现一个变量槽,这是由具体的虚拟机实现自行决定的
  • 这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果 Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,或者新建 Java 虚拟机栈时发现内存不足会抛出 OutOfMemoryError 异常
  • 详细了解虚拟机栈可以阅读这篇文章JVM 字节码执行引擎中的运行时栈帧结构部分

本地方法栈

  • 与虚拟机栈的作用类似,虚拟机栈为执行 Java 方法服务,本地方法栈为执行本地方法服务
  • 具体的虚拟机可以自由实现,有的 Java 虚拟机(例如 Hot-Spot 虚拟机)甚至直接把虚拟机栈和本地方法栈合二为一
  • 和虚拟机栈相同,在相同的情况下抛出 StackOverflowError 异常和 OutOfMemoryError 异常

Java 堆

  • 虚拟机所管理的内存中最大的一块,线程共享
  • 唯一目的就是存放对象实例
  • Java 中几乎所有对象实例都在这里分配内存,实际上逃逸分析等技术的发展使得对象实例并不全部都分配在堆上
  • 是垃圾收集器管理的内存区域
  • 所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提升对象分配时的效率
  • Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java虚拟机都是按照可扩展来实现的
  • 如果在 Java堆中没有内存完成实例分配,并且堆也无法再扩展时, Java虚拟机将会抛出 OutOfMemoryError 异常

方法区

  • 线程共享
  • 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
  • 类的方法存储在方法区,是方法的静态表现。方法的执行过程存储在栈帧,是方法的动态表现。
  • 和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展,但可以选择不实现垃圾收集
  • 如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常

运行时常量池

  • 是方法区的一部分
  • Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
  • 运行时常量池相对于 Class 文件常量池的一个重要特征是具备动态性,即运行期间也可以将新的常量放入池中
  • 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常

直接内存

  • 不是运行时数据区的一部分
  • 本机直接内存的分配不会受到 Java 堆大小的限制,但是肯定还是会受到本机总内存大小以及处理器寻址空间的限制,如果配置虚拟机参数时,根据实际内存去设置但忽略直接内存,会使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现 OutOfMemoryError 异常

虚拟机对象

对象的创建

类加载检查

分配内存

为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间实际上等于从 Java堆中划分出一块确定大小的内存块。

如果 Java 堆中内存是绝对规整的,可以使用“指针碰撞”的分配方式(所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离)

如果 Java 堆中的内存不是规整的,就需要使用“空闲列表”的分配方法(维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录)

选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定

另一个需要考虑的问题是在并发情况下对象创建的线程安全问题。一个方案是 CAS 配上失败重试保证更新操作的原子性,另一个方案是使用线程私有的分配缓冲区(TLAB),先在本线程的本地缓冲区分配,缓冲区用完了才需要同步锁定

对象设置

设置对象头等内容

构造函数

从虚拟机的视角对象已经产生了,但是从 Java 程序的视角构造函数还没有执行,对象创建才刚刚开始

对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)

对象头

  • 对象自身的运行时数据
  • 类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
  • 如果对象是一个 Java数组,那在对象头中还必须有一块用于记录数组长度的数据

实例数据

在程序代码里面所定义的各种类型的字段内容

从父类继承下来的,和在子类中定义的字段都必须记录起来

这部分的存储顺序会受到虚拟机分配策略参数和字段在 Java 源码中定义顺序的影响

HotSpot虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),在此基础上,父类中定义的变量在子类之前

对齐填充

HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,所以任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全

对象的访问定位

Java 通过栈上的 reference 数据来操作堆上的具体对象

reference 类型的具体实现由虚拟机决定,主流的对象访问方式有两种:使用句柄和直接指针

句柄访问

通过句柄访问对象

Java 堆中划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息

优点是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改

直接指针访问

通过直接指针访问对象

reference 中存储的直接就是对象地址

优点是速度更快,节省了一次指针定位的时间开销

就 HotSpot 而言主要使用直接指针访问对象的方式进行对象访问

最后修改:2023 年 06 月 05 日
如果觉得我的文章对你有用,请随意赞赏