深入浅出 JVM 之运行时数据区

深入浅出 JVM 之运行时数据区

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

系统环境:

  • JDK 1.8

参考地址:

深入浅出 JVM 系列文章

一、什么是 JVM 运行时数据区

Java 虚拟机 (JVM) 可以分为三个主要的子系统,分别为 类加载器子系统运行时数据区执行引擎

Java 虚拟机子系统

类加载子系统 完成了 加载验证准备解析初始化 等几个阶段后,执行引擎便开始对这些初始化完成的类进行使用。

Java 虚拟机执行流程

在操作系统中,每个进程通常会被分配一个虚拟的内存空间,进程的操作都在这个内存空间中进行管理。而 Java 虚拟机作为一个进程,也同样会获得操作系统分配的内存空间。然而,若 Java 虚拟机直接使用系统分配的内存空间来为对象分配空间,而不考虑对象的类型和内存的连续性的话,那么这块内存空间将会很快被占满,并且还会产生很多内存碎片,导致空间难以管理和清理。

所以,为了更有效地管理内存,Java 虚拟机对内存空间进行了划分,创建了 运行时数据区,将不同生命周期和大小的对象分配到不同的内存区域,并使用垃圾回收机制 (GC) 来回收无效的内存空间,从而减少内存泄漏的可能性。Java 虚拟机中的运行时数据区包括 方法区虚拟机栈本地方法栈程序计数器,如下图所示:

运行时数据区 (Rumtime Data Area)

在学习 Java 虚拟机时,看到别人资源中对运行时数据区有个很形象的比喻: 如下图所示的厨房,我们可以把大厨后面的东西 (切好的菜、刀、调料) 比作是运行时数据区,而厨师则类比于执行引擎,把准备好的工具与食材制作成精美的菜品。

厨房比喻

二、运行时数据区的组成

根据 《Java虚拟机规范》 的规定,如果按照 线程共享线程私有 划分,那么 Java 虚拟机所管理的内存空间将会包括以下几个运行时数据区:

  • 线程共享: 堆、方法区
  • 线程私有: 程序计数器、虚拟机栈、本地方法栈
运行时数据区-线程共享与私有区域

2.1 线程私有区域

(1) 程序计数器

程序计数器(Program Counter Register) 是 JVM 运行时数据区中某个线程的一块私有内存区域,可以将它看做是当前线程所指向的字节码的行号指示器。

在 JVM 的概念模型中,字节码解释器通过改变程序计数器的值来选择下一条需要执行的字节码指令。它是程序控制流的指示器,依赖它来完成如 分支循环跳转异常处理线程恢复 等基础功能。

每当创建一个线程时,就会创建一个程序计数器,用于记录线程执行到的字节码指令位置,这样可以确保当前线程获得 CPU 时间片执行时,能够恢复到正确的执行位置。

  • 注:
  • ① 如果线程正在执行的是一个 Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令的地址。
  • ② 如果正在执行的是本地方法 (Native Method),则这个计数器值为空 (Undefined)
  • ③ 程序计数器是唯一一个在《Java虚拟机规范》中,没有规定任何 OutOfMemoryError 情况的区域。

(2) 虚拟机栈

虚拟机栈(Java Virtual Machine Stack) 也是 JVM 运行时数据区中某个线程的一块私有内存区域,每当创建一个线程时就会创建一个虚拟机栈,用于存储 线程的栈帧。而 栈帧 则是用于存储 局部变量表操作数栈动态连接方法出口 等信息。

每当线程调用方法时就会创建一个栈帧,并且在栈帧中执行代码、存储中间变量和局部变量,当线程调用方法结束后就会使栈帧出栈,销毁栈帧中的数据。

  • 注:
  • 在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:
  • ① 如果线程请求的栈深度超出了虚拟机所允许的最大深度,则会抛出 StackOverflowError 异常;
  • ② 如果 JVM 栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常;

(3) 本地方法栈

本地方法栈 (Native Method Stacks)虚拟机栈 类似,都是用于存储线程执行方法时的数据。不过虚拟机栈只在线程调用 Java方法 时起作用,而本地方法栈则在调用 Native 方法时起作用。

  • 注:
  • ① 本地方法栈也会在栈深度溢出或者栈扩展失败时,分别抛出 StackOverflowError 和 OutOfMemoryError 异常。
  • ② 在《Java虚拟机规范》中,并没有对本地方法栈中方法使用的语言、方式与数据结构进行任何强制规定,因此具体的虚拟机可以根据需要自由实现它。

2.2 线程共享区域

(1) 堆

堆(Heap) 是在 JVM 启动时创建的,是 JVM 运行时数据区中最大的一块线程共享的内存区域。而且 的内存空间在逻辑上连续,但物理上不一定连接,其主要用于存储 Java 中的 对象实例,几乎所有的对象实例都存放在堆中。

堆空间是垃圾回收器管理的内存区域,从回收内存的角度来看,由于现在的垃圾收集器大部分都是基于分代收集理论设计的,所以大部分资料都会将管理对象的内存区域划分为 新生代老年代永久代 等。不过这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个 JVM 具体实现的固有内存布局,更不是《Java虚拟机规范》对堆的进一步细致划分。

如果从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer, 简称 TLAB),以提升对象分配时的效率。不过无论从什么角度进行划分,都不会改变 Java 堆中存储内容的共性,无论是哪个区域存储的都只能是对象的实例。所以,将 Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

  • 注:
  • ① 如果在 Java 堆中没有足够的内存空间进行实例分配,并且堆也无法再扩展时,JVM 将会抛出 OutOfMemoryError 异常。
  • ② 主流的 HotSpot 虚拟机中存在多种类型的垃圾回收器,如 Serial、ParNew、Parallel、CMS、G1 和 ZGC 等,大部分都是采用分代模式进行设计的,不过随着设计的进步,HotSpot 虚拟机中也出现了不采用分代设计的新垃圾收集器,所以堆空间并不能完全按照 "新生代"、"老年代"、"永久代" 等进行划分。

(2) 方法区

方法区(Method Area) 类似,是在 JVM 启动时创建的,也是 JVM 运行时数据区中的一块线程共享的内存区域。方法区的内存空间在逻辑上连续,但物理上不一定连续,主要用于存储一些 类信息方法信息域信息JIT代码缓存运行时常量池

提到方法区就需要说一下 HotSpot 虚拟机中的永久代 (Permanent Generation) 概念。在 JDK 7 及以前,方法区的实现为永久代,但永久代的设计容易引发内存溢出 OOM 错误,导致程序崩溃。因此在 JDK 6 版本时期,HotSpot 开发团队开始放弃永久代,逐步改用本地内存实现方法区。

在 JDK 7 以后,HotSpot 团队将 字符串常量池静态变量 等从 永久代 移除,转移到了 中。而在 JDK 8 以后,永久代的概念被完全废弃,使用 元空间(Metaspace) 替代永久代。元空间直接使用操作系统内存空间,所以其大小仅受操作系统内存大小的限制,从而减少内存溢出错误的可能性。

  • 注:
  • ① 如果方法区中的内存无法满足分配请求,JVM 将抛出一个 OutOfMemoryError 错误。
  • ② 《Java虚拟机规范》对方法区的约束非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。

---END---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。