运行时数据区:
在 Java 虚拟机(JVM)执行程序时,内存被划分为多个数据区:
-
线程私有区域:1. 程序计数器 2.Java 虚拟机栈 3.本地方法栈
-
线程共享区域:1.Java 堆 2.方法区
程序计数器:
程序计数器是一块很小的内存区域,用于记录当前线程正在执行的字节码的行号。通过不断改变自身的值来确定下一条要执行的指令。
Java 虚拟机栈:
描述Java 方法在执行时的内存模型。每个线程启动时都会分配一个独立的栈空间,线程结束后栈空间被回收。栈的生命周期与线程相同。
- 栈帧:每次方法调用时,都会创建一个栈帧,栈帧用于存储方法的局部变量表、操作数栈、动态链接和方法出口等信息。方法的执行过程就是栈帧从入栈到出栈的过程。局部变量表:
- 局部变量表:存储方法的局部变量和参数。
- 操作数栈:在方法执行过程中用于操作数的存储和运算。
- 动态链接:在运行时将符号引用解析为具体的内存地址。
- 方法出口:管理方法调用的返回地址,确保调用链的正确执行。
- 异常处理:
- 当线程请求的栈深度超过虚拟机允许的深度时,会抛出
StackOverflowError
。- 无限循环/递归
- 如果栈可以动态扩展,而扩展时无法申请到足够的内存,会抛出
OutOfMemoryError。
- 发生在程序试图分配一个对象时,但堆内存已被使用完。例如,程序不断创建对象但没有释放,或加载了大量的大对象(如图片、大型数据集等)。
- 当线程请求的栈深度超过虚拟机允许的深度时,会抛出
本地方法栈:
服务本地方法:本地方法栈的作用与虚拟机栈类似,但它专门为本地方法(通常是用 C 或 C++ 编写的代码)服务。当调用本地方法时,虚拟机栈保持不变,直接调用指定的本地方法。
- 异常处理:
- 和虚拟机栈一样,本地方法栈在栈深度超过允许范围时抛出
StackOverflowError
。 - 当栈扩展失败时(如果支持动态扩展),抛出
OutOfMemoryError
。
- 和虚拟机栈一样,本地方法栈在栈深度超过允许范围时抛出
堆:
堆 是虚拟机内存管理中最大的一块区域,所有线程共享,在虚拟机启动时创建。堆的主要作用是存放对象实例,几乎所有 Java 对象实例都是在堆中分配内存的。堆可以是物理上不连续的内存空间,但在逻辑上应被认为是连续的,特别是对于数组等大对象,虚拟机会要求连续的内存空间以提高存储和访问效率。
- 内存分配:堆既可以是固定大小,也可以是可扩展的。可以通过
-Xms
和-Xmx
参数设置堆的最小和最大容量。当前主流 JVM 实现都支持堆的动态扩展。 - 内存不足:如果堆内存不足以分配新的对象实例且无法扩展时,虚拟机会抛出
OutOfMemoryError
异常。
方法区:
方法区 是 JVM 用于存储已加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据的内存区域。
-
JDK 版本的变化:
- JDK 8 之前:方法区由永久代(PermGen)实现。永久代有固定大小的上限(可以通过
-XX:MaxPermSize
设置),容易发生内存溢出(OutOfMemoryError
)。 - JDK 7:开始将字符串常量池、静态变量等从永久代移出,减少了永久代的压力。
- JDK 8:永久代被完全废弃,方法区改由元空间(Metaspace)实现,元空间使用本地内存,并且可以动态调整大小,以避免内存溢出。
- JDK 8 之前:方法区由永久代(PermGen)实现。永久代有固定大小的上限(可以通过
运行时常量池:
- 方法区的一部分,存储在类加载过程中从 Class 文件中提取的常量信息,包括字面量和符号引用。它在程序运行时提供了一种灵活且动态的方式来管理常量。
-
字面量和符号引用:Class 文件包含了常量池表,用于存储编译时生成的各种字面量(如字符串、数字常量等)和符号引用(如类名、方法名、字段名等)。这些内容在类加载后被放入运行时常量池中。
-
动态性:运行时常量池的一个重要特征是其动态性。Java 允许在程序运行期间向运行时常量池中添加新常量,而不局限于编译期生成的常量。一个典型的例子是
String
的intern()
方法,它可以将字符串在运行时添加到常量池中。 -
内存限制:运行时常量池受方法区内存的限制,当常量池无法再分配内存时,会抛出
OutOfMemoryError
异常。这意味着,如果程序大量使用常量池或动态添加大量常量,可能会出现内存溢出的问题。