运行时数据区
原创大约 6 分钟
运行时数据区
运行时数据区是指在运行程序时存储数据的内存区域。分为程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区五个部分。
- 线程私有:
- 程序计数器 - 存储线程执行位置
- 虚拟机栈 - 存储Java方法调用与执行过程的数据
- 本地方法栈 - 存储本地方法的执行数据
- 线程共享:
- 堆 - 主要存储对象
- 方法区 - 存储类/方法/字段等定义(元)数据
- 运行时常量区 - 保存常量static数据
程序计数器
程序计数器是线程私有的,用于记录当前程序执行的字节码指定位置。
知识点:
- 线程私有
- 不会被垃圾回收
- 访问速度最快(JVM内存区域中)
- 占用内存少,不会出现
OutOfMemoryError
- 执行Java方法时,记录的是字节码指令地址
- 执行Native方法时,记录为未定义(
undefined
)
思考以下问题,加强理解:
- 程序计数器如何保证线程能够准确地恢复到之前的执行位置?
- 字节码执行与程序计数器的关系?
虚拟机栈
虚拟机栈是线程私有的内存区域,其生命周期与线程相同。 它描述了方法执行的内存模型。当方法被执行时,JVM会为该方法同步创建一个栈帧(Stack Frame)。
知识点:
- 线程私有
- 不会被垃圾回收
- 访问速度仅次于程序计数器
- 栈大小可设置,限制深度:
- 推荐固定大小设置(
-Xss 数值[k|m|g]
),达到上限,抛出StackOverflowError
- 动态扩展,可用内存不足时,抛出
OutOfMemoryError
- 推荐固定大小设置(
栈帧的内部结构:
- 局部变量表: 用于存储方法中的局部变量和参数。
- 操作数栈: 后进先出(LIFO)结构,用于方法执行时存储执行指令产生中间结果。
- 动态链接: 指在方法调用时,将符号引用转换为直接引用的过程。
- 方法返回地址: 指方法调用后返回位置的地址。
本地方法栈
本地方法栈是线程私有,与虚拟机栈功能相似。其中虚拟机栈为Java方法(字节码)服务,本地方法栈则为Native方法服务。
- HotSpot虚拟机把虚拟机栈和本地方法栈合二为一。
- 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出
StackOverflowError
和OutOfMemoryError
异常。
Java堆
Java堆是虚拟机管理的内存中最大的一块,线程共享,并在虚拟机启动时创建。 它的唯一目的是存放对象实例,几乎所有的对象实例以及数组都在堆上分配。
堆内存模型:
现代垃圾收集器采用分代收集理论进行设计,因此堆内存被划分为多个区域,包括:
- 新生代:
- 存放生命周期较短的对象
- 通常由Eden区和两个Survivor区(被称为from/to或s0/s1)组成,默认比例是
8:1:1
- 填满时触发
Minor GC
(小型垃圾回收) - 采用复制算法,将存活的对象复制到Survivor区,然后清理Eden区和使用过的Survivor区。
- 老年代:
- 存放生命周期较长,或多次垃圾收集后任然存活的对象
- 填满时触发
Major GC
或Full GC
,耗时严重 - 使用的垃圾收集算法通常是标记-清除算法或标记-整理算法。
- 永久代(PermGen):
- 存放Class元数据,包括类结构、方法、字段信息等
- 属于“堆”的一部分,无法扩展时会抛出
OutOfMemoryError
异常 - 通过命令
-Xms
设置初始堆大小,-Xmx
设定最大堆大小 - 从JDK8开始,被元空间(Metaspace)取代,称为“非堆”,使用的是本地内存
DigitalOcean——Java (JVM) 内存模型 - Java 中的内存管理
方法区
方法区是JVM规范中的一个逻辑区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
- 在Java7的时候,方法区被称为“永久代(PermGen)”。
- 从Java8开始,方法区的实现被改为元空间(Metaspace),元空间使用的是本地内存,而不是像永久代那样在JVM的堆内存中分配。
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量与符号引用,支持在运行时动态添加新的常量。
- 字面量: 表示固定的数据值,如整数、浮点数、字符串等常量。
- 符号引用: 一组符号,用于描述所引用的目标,包括类和接口的全限定名、字段和方法的名称。
知识点:
- 具备动态性,如
String.intern()
方法将字符串对象添加到运行时常量池中。 - 会产生
OutOfMemoryError
异常
思考以下问题,加强理解:
- Class常量池与运行时常量池的关系?
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也未在《Java虚拟机规范》中明确定义。 然而,由于其频繁使用且可能导致OutOfMemoryError
异常,值得在此进行讨论。
关键点:
- NIO的引入: JDK 1.4引入了NIO(New Input/Output)类,通过通道(Channel)和缓冲区(Buffer)实现了一种新的I/O方式。它使用本地(Native)函数库直接分配堆外内存,并通过在Java堆中的
DirectByteBuffer
对象进行引用和操作。 - 性能优势: 这种方法能够显著提高性能,因为它避免了在Java堆和本地堆之间的数据复制,从而加快了I/O操作。
- 内存限制: 虽然直接内存的分配不受Java堆大小的限制,但仍受到本机总内存(包括物理内存、SWAP分区或分页文件)大小和处理器寻址空间的限制。
- 配置问题: 在配置虚拟机参数(如
-Xmx
)时,管理员通常会根据实际物理内存来设置Java堆的大小,但可能忽略直接内存的占用。如果各个内存区域的总和超过了物理内存限制,可能在动态扩展时导致OutOfMemoryError
异常。