Java内存结构
Java内存结构主要分为5个部分:
- 线程私有的:程序计数器、虚拟机栈、本地方法栈
- 线程共享的:堆、方法区
线程私有
程序计数器
程序计数器(Program Counter, PC用于存储当前线程正在执行的字节码指令地址。
字节码执行过程中,程序计数器的值会随着程序的执行而不断更新,以保证程序能顺序执行、控制分支跳转或循环。
同时,线程切换后能恢复到正确的执行位置,也依赖于程序计数器
如果线程正在执行的是本地方法(Native Method),程序计数器的值为undefined,因为本地方法不通过字节码执行。
虚拟机栈
虚拟机栈主要用于存储方法调用时的栈帧,栈帧主要由局部变量表、操作数栈、动态链接、方法返回地址组成。每次调用方法时,JVM会为该方法分配一个栈帧,方法执行完成后栈帧出栈。
当栈的深度超过虚拟机栈的最大深度(通常是由于递归或死循环导致的),会抛出StackOverflowError
。
栈帧的主要组成
- 局部变量表:包括方法的参数、局部变量(基本类型、对象引用)
- 操作数栈:存放方法执行过程中产生的中间计算结果和临时变量。
- 动态链接:方法调用时,将符号引用解析为实际内存地址的过程。(是 JVM 处理多态、继承、接口等语言特性的基础)
- 方法返回地址:调用一个方法时,记录调用方在方法调用之后的下一条指令的地址
动态链接和静态链接
特性 | 静态链接 | 动态链接 |
---|---|---|
链接时机 | 类加载阶段(如 static 方法和 private 方法) | 程序运行时 |
确定性 | 引用在加载时已确定,运行时无需解析 | 引用在运行时解析,支持动态绑定 |
性能 | 运行时性能较高,但缺乏灵活性 | 运行时有一定开销,但支持多态和动态特性 |
适应性 | 代码更新需要重新加载类 | 代码更新可以在运行时生效,提高系统适应性 |
本地方法栈
本地方法栈 和虚拟机栈类似,但它是专门为本地方法(Native Methods) 设计的。
Java 可以通过 JNI(Java Native Interface)调用 C/C++、汇编等语言编写的本地方法,JVM 会将这些方法调用放在本地方法栈中执行。
在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一。线程的虚拟机栈其实是用来管理 Java 方法调用和本地方法调用的栈空间。当一个线程在执行 Java 方法时,该方法的栈帧会压入虚拟机栈;当该 Java 方法中调用了本地方法时,本地方法的栈帧也会压入同一个虚拟机栈中。
线程共享
堆
JVM中最大的内存区域,是JVM的主要垃圾回收区域。用于存储对象实例和数组,所有的对象都在堆中分配内存。
JVM的堆区通常被分为年轻代和老年代。年轻代又细分为Eden、Survivor区。
在JDK7还有永久代,JDK8移除了永久代,改为使用直接内存的Matespace。永久代(PermGen)和元空间(Metaspace)都是JVM规范中方法区(Method Area)的实现。
方法区
主要存储 类的元数据、常量、静态变量和方法的字节码。在JDK 8之后,方法区的实现 元空间
使用本地内存,不再属于堆。
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
常量池、运行时常量池、字符串常量池的关系
- 常量池是编译期由Class文件中
Constant Pool Table
生成的一部分,存储了类或接口中的常量。 - 运行时常量池是常量池的运行时表示,它在类加载时从
.class
文件中的常量池中加载,属于方法区
的一部分。 - 字符串常量池在堆上,运行时常量池保存在元空间中