运行时数据区
提示
运行时数据区 是指在程序运行的过程中,由 JVM 管理的各种内存区域,这些区域主要用于存储程序运行时所需要的数据,包括程序的字节码、类信息、对象实例、方法等。运行时数据区主要包括以下几个部分:程序计数器(Program Counter)、Java 虚拟机栈(JVM Stack)、堆(Heap)、方法区(Method Area)、运行时常量池(Runtime Constant Pool)、本地方法栈(Native Method Stack)

概述
上一章中分析完了类加载的过程,当 IO 流经过了 加载 -> 验证 -> 准备 -> 解析 -> 初始化 等阶段后,信息就驻留在运行时数据区的堆中,以便于执行引擎在程序运行时使用。简单来说, 运行时数据区可以理解为 JVM 的工作内存。所有加载到内存中的类、创建的对象、方法调用的现场信息以及程序执行的轨迹,都分布在运行时数据区不通的区域中。根据《Java 虚拟机规范》,这些区域有些随着 JVM 进程的启动而一直存在,有些则随着用户线程的创建和销毁而建立和消失。从线程的角度看,运行时数据区可以分为两大类:线程私有 和 线程共享
进程和线程
TIP
为了方便记忆线程私有和共享,这里只简单提一嘴线程和进程的概念,如有需要后续专门开一篇计算机基础来详细讲解。
进程:资源分配与管理的独立单元
进程和线程程是操作系统中最基础、最重要的概念之一。它被定义为 程序的一次执行过程,是系统执行资源分配和调度的独立单位。可以将进程想象成一个正在运行的应用程序实例,例如你正在使用 IDEA 写代码 同时你正在调试你写的程序,那么 IDEA 视为一个进程,你的程序也视为一个进程,两者之间相互独立。
线程: CPU 调度与执行的基本单位
线程存在的作用是为了进一步提高系统的并发行和效率,降低创建和切换的时空开销,线程是操作系统在进程内部引入的更细粒度的概念。线程是进程中的一个实体, 是操作系统能够进行运算调度的最小单位。如果说进程是一个 工厂 ,那么线程就是工厂里的 流水线工人。一个工厂(进程)可以有多条流水线(线程)同时工作,共享工厂的场地、原谅(进程资源),但每条流水线有自己独立的工作台和操作手册(线程私有数据)
JVM 系统线程:Java 世界的幕后工作者
在 Java 世界中,我们通过 java.lang.Thread 类来创建和管理Java 线程。然而,Java 线程需要映射到操作系统的原生线程才能真正执行。以最主流的 Hotspot JVM 实现为例,每个 Java 线程都与一个操作系统的本地线程直接映射,当 Java 线程启动时,JVM 会请求操作系统创建一个对应的本地线程;当 Java 线程结束时,本地线程也随之被回收。
除了我们编写的应用程序线程,JVM 自身在后台同样维护着一系列至关重要的 系统线程(守护线程) ,它们保证了 JVM 的稳定、高效运行。主要的后台线程包括:
- 虚拟机线程: 这是 JVM 的指挥中心。它等待 JVM 到达 安全点(SafePoint),然后执行一些需要停止世界(Stop-The-World) 的关键操作,例如某些类型的垃圾回收、线程栈收集、线程挂起和偏向锁撤销。这些操作必须在一个独立的、可控的线程中进行,以确保堆内存状态的一致性
- 周期任务线程: 相当于 JVM 内部的定时器。它负责处理定时器事件,用于调度一些周期性的操作。
- GC 线程: 垃圾回收线程。JVM 中不同的垃圾收集器(如 Serial、Parallel、CMS、G1 等)都有对应的 GC 线程来负责内存的回收工作,这是 Java 自动内存管理的基石。
- 编译线程: JVM 性能优化的引擎。这些线程在程序运行时,将热点代码(频繁执行的方法)的字节码动态编译成本地平台相关的机器码(即使编译, JIT),从而大幅度提升执行速度。
- 信号调度线程: JVM 与操作系统之间的通讯员。它负责接收操作系统发送给 JVM 的各种信号,并调用 JVM 内部适当的方法进行处理
线程私有区域
这类区域的生命周期与线程相同,每个线程都拥有自己独立的一份,它们之间互不干扰。保证了多线程环境下程序执行的正确性
- 程序计数器: 这是一块很小的内存空间,可以看作是 当前线程所执行的字节码的行号指示器 。字节码解释器通过改变它的值来选取下一条需要执行的指令。由于 Java 的多线程是通过线程轮流切换并分配处理器时间片来实现的,为了在线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。它也是唯一一个在 《Javm 虚拟机规范》中没有规定任何
OutOfMemoryError情况的区域 - Java 虚拟机栈: 每个方法在执行时,JVM 都会同步创建一个 栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用到执行完毕,就对应一个栈帧在虚拟机中从入栈到出栈的过程。因此虚拟机栈描述的是 Java 方法执行的线程内存模型。如果线程请求的栈深度超出虚拟机运行的最大深度,则抛出异常
StackOverflowError异常;如果栈可以动态扩展但无法申请到足够的内存,则会抛出OutOfMemoryError异常。 - 本地方法栈: 其作用与虚拟机栈非常相似,区别在于 虚拟机栈执行 Java 方法,本地方法栈则执行 Native 方法服务。在 Hotspot 虚拟机中,本地方法栈和虚拟机栈是合二为一的。
线程共享区域
这类区域被所有线程共享,在虚拟机启动时创建,随着虚拟机退出而销毁。
Java 堆: 这是 JVM 所管理的内存中最大的一块,用于存放几乎所有的对象实例和数组。堆是垃圾回收的主要区域,因此也被称做GC 堆。从内存回收角度看,现代收集器大多基于分代收集理论,将堆划分为新生代、老年代等;从内存分配角度看,所有线程共享的堆中可以划分出多个线程私有的分配缓存区以提高效率。如果堆中没有内存完成实例分配且无法再扩展时,将抛出
OutOfMemoryError异常。方法区: 用于存储 已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据。方法区在逻辑上是堆的一部分,但是为了与 Java 堆区分,常被称为非堆。在 JDK8 及以后,Hotspot 虚拟机使用元空间替代了永久代来实现方法区。当方法区无法满足新的内存分配需求时,也会抛出
OutOfMemoryError异常- 运行时常量池: 它是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用,这部分内容在类加载后进入方法区的运行时常量池中。运行时常量池的一个重要特性是动态性,并非只有编译期才会产生新的变量,运行期间也可以将新的常量放入池中,例如
String.intern()方法。
- 运行时常量池: 它是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用,这部分内容在类加载后进入方法区的运行时常量池中。运行时常量池的一个重要特性是动态性,并非只有编译期才会产生新的变量,运行期间也可以将新的常量放入池中,例如
直接内存(非运行时数据区)
直接内存并不是《Java 虚拟机规范》中定义的运行时数据区的一部分。但它也被频繁使用,例如在 NIO(New I/O) 中,可以使用 Native 函数库 直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。直接内存的分配不会受到 Java 堆大小的限制,但如果各个内存区域总和大于物理内存限制,也可能会导致 OutOfMemoryError 异常