文章目录
- JVM的组成
- 加载字节码流程
- 运行时数据区-总览
- 1. 程序计数器
- 2. 虚拟机栈
- 栈帧
- 栈的运行原理
- 3. 本地方法栈
- 4. 堆内存(Java Heap
- 虚拟机对堆 的划分
- 1. 年轻代(Young Generation):
- 2. 老年代(Old Generation):
- 3. 永久代/元空间(Permanent Generation/Metaspace):
- 注意事项:
- 对象在堆中的生命周期
- 1. **创建阶段(Creation):**
- 2. **引用阶段(Reference):**
- 3. **使用阶段(Usage):**
- 4. **不可达阶段(Unreachable):**
- 5. **垃圾收集阶段(Garbage Collection):**
- 注意事项:
- 5. 方法区(Method Area
- 主要存储
JVM的组成
JVM 指的是 Java 虚拟机(Java Virtual Machine),它是 Java 程序的运行环境。JVM 可以在不同的操作系统上执行 Java 字节码,这使得 Java 程序具有跨平台性,只需要编写一次,就可以在任何安装了 Java 运行时环境(JRE)的地方运行。
主要包括以下几个方面:
-
类加载器(Class Loader):负责将类的字节码文件加载到内存中,并生成对应的 Class 对象。
-
运行时数据区(Runtime Data Area):包括方法区、堆、栈、程序计数器和本地方法栈等。这些区域用于不同类型的数据存储和操作,比如堆用于存储对象实例,栈用于存储局部变量和方法调用。
-
执行引擎(Execution Engine):负责执行编译后的字节码文件。它可以通过解释器执行字节码,也可以通过即时编译器将字节码编译成本地机器代码执行。
-
本地方法接口(Native Interface):JVM 提供了与本地方法库进行交互的接口,允许 Java 调用本地方法或者本地方法库调用 Java 方法。
-
本地方法栈(Native Method Stack):用于执行本地方法,与 Java 虚拟机栈相对应。
加载字节码流程
-
加载(Loading):通过类加载器(Class Loader)将字节码文件加载到内存中。类加载器负责在类路径下查找并加载字节码文件,然后生成对应的 Class 对象。
-
验证(Verification):对加载的字节码文件进行验证,确保其格式符合 JVM 规范,不会危害虚拟机的安全。
-
准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值。
-
解析(Resolution):将类、接口、字段和方法的符号引用解析为直接引用,这个过程可以在运行期间延迟到真正需要的时候进行。
JVM 加载字节码文件的流程包括类加载器、运行时的数据区域、执行引擎、本地接口和本地方法栈等组成部分,它们在以下步骤中发挥作用:
-
类加载器(Class Loader):
- 类加载器负责将字节码文件加载到内存中。它首先根据类的全限定名找到对应的字节码文件,并将其读取到内存中。
- 类加载器还会对加载的字节码文件进行验证,确保其格式符合 JVM 规范,不会危害虚拟机的安全。
-
运行时数据区域(Runtime Data Area):
- 方法区:用于存储类的结构信息,包括类的字段、方法、常量池等。类加载器在加载字节码文件时,会将这些信息存储在方法区中。
- 堆:用于存储对象实例。当类加载器加载字节码文件后,会在堆中为类的实例分配内存空间。
- 栈:用于存储局部变量和方法调用。当执行引擎执行字节码时,会使用栈来保存方法的局部变量以及方法调用的信息。
- 程序计数器:用于线程之间切换和记录方法执行的位置。程序计数器指向当前正在执行的字节码指令。
-
执行引擎(Execution Engine):
- 执行引擎负责执行字节码文件中的指令。它可以通过解释器执行字节码,逐条解释并执行指令。
- 对于经过热点代码的方法,执行引擎还可以使用即时编译器将字节码编译为本地机器代码,以提高执行效率。
-
本地方法接口(Native Interface):
- 如果字节码文件中包含调用本地方法的指令,JVM 将使用本地方法接口与本地方法库进行交互。
- 本地方法接口允许 Java 调用本地方法或者本地方法库调用 Java 方法,实现了 Java 与底层系统的交互。
-
本地方法栈(Native Method Stack):
- 当执行本地方法时,JVM 会使用本地方法栈来执行本地方法。本地方法栈与 Java 虚拟机栈相对应,用于执行本地方法所需的操作。
在整个加载字节码文件的流程中,类加载器负责加载和验证字节码文件,运行时数据区域存储了加载的类信息和对象实例,执行引擎执行字节码指令,本地接口实现 Java 与本地方法的交互,本地方法栈用于执行本地方法。这些组成部分相互协作,使得 JVM 能够正确加载和执行字节码文件。
运行时数据区-总览
- 线程不共享(独享)
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 线程共享
- 方法区
- 栈
接下来我们对内存区域进行剖析
1. 程序计数器
-
虚拟机中的一块内存空间,它是线程私有的,也即每个线程都有自己独立的程序计数器。程序计数器可以看作是当前线程所执行的字节码的行号指示器,即它指向当前线程正在执行的字节码指令的地址。在Java虚拟机规范中,程序计数器是唯一一块在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
-
在程序计数器中,存储着当前线程正在执行的 Java 字节码指令的地址,或者正在执行的本地方法的指令地址。由于程序计数器是线程私有的,因此它不会发生线程切换时的数据同步问题。
-
程序计数器在线程切换、线程恢复以及指令重复执行等方面发挥着重要作用。在多线程环境下,程序计数器能够确保线程在切换后能够正确恢复到之前的执行位置,从而保证了程序的正常执行。
与操作系统的一些区别:
- 操作系统程序计数器(OS 程序计数器):在操作系统中,程序计数器是处理器中的一个寄存器,用于存储当前正在执行的指令的地址或者下一条即将执行的指令的地址。它是处理器的一部分,用于支持指令级的控制流。
- Java 虚拟机程序计数器(JVM 程序计数器):在 Java 虚拟机中,程序计数器是线程私有的内存区域,用于存储当前线程正在执行的字节码指令的地址。
2. 虚拟机栈
用于存储线程的方法调用和局部变量。每个线程在创建时都会被分配一个对应的虚拟机栈,用于跟踪线程的方法调用和执行情况。
开始分析前,还需要了解一些概念
- 栈帧:每个方法在被调用时都会创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量表、操作数栈、动态链接、返回地址等信息。
- 栈深度限制:虚拟机栈对方法的调用深度有一定的限制,当方法调用层次过深时,会抛出 StackOverflowError 异常。
- 栈内存大小:虚拟机栈的内存大小可以通过启动参数进行调整
- 这也是重要的调优手段之一
栈帧
我们再对栈帧具体探索:
栈帧(Stack Frame)是虚拟机栈中的一个重要概念,它包含了方法在执行过程中所需的各种信息。以下是栈帧通常包含的信息:
-
局部变量表(Local Variable Table):用于存储方法中的局部变量,包括方法参数、方法内部定义的变量等。局部变量表中的每个元素都可以存储一个基本数据类型或者一个对对象的引用。
-
操作数栈(Operand Stack):操作数栈用于存储方法执行过程中的操作数,例如进行算术运算时需要使用的数值。方法执行时会从局部变量表中获取数据,进行计算后将结果存入操作数栈中。
-
动态链接(Dynamic Linking):指向运行时常量池中该方法的引用,通过动态链接可以在运行时解析调用的方法、字段等。
-
返回地址(Return Address):记录了方法调用结束后需要返回的指令地址,用于恢复到方法调用点继续执行。
-
附加信息:栈帧中还可能包含一些额外的附加信息,例如异常处理相关的信息、调试信息等。
栈的运行原理
-
方法调用: 当一个方法被调用时,一个新的栈帧被压入栈顶。这个栈帧包含了方法的参数、局部变量以及用于存储中间计算结果的操作数栈。
-
栈的压栈和弹栈: 方法调用时,栈帧被压入栈顶;方法返回时,栈帧被弹出。这种后进先出(LIFO)的结构保证了方法的调用和返回的顺序。
-
局部变量表: 每个栈帧中包含一个局部变量表,用于存储方法中的局部变量。包括方法参数、方法内部定义的局部变量等。
- 栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot) ,long和double类型占用两个槽,其他类型占用一个槽
当一个方法被执行时,Java 虚拟机会在虚拟机栈中创建一个对应的栈帧(Stack Frame)来存储该方法的局部变量和部分运行时数据。让我们通过一个简单的示例来说明虚拟机栈的作用:
假设有以下的 Java 代码:
public class StackExample {
public static void main(String[] args) {
int result = addNumbers(3, 5);
System.out.println("Result: " + result);
}
public static int addNumbers(int a, int b) {
int sum = a + b;
return sum;
}
}
当程序执行到 addNumbers(3, 5)
方法调用时,会发生以下操作:
-
JVM 虚拟机栈为
addNumbers
方法的执行创建一个新的栈帧,用于存储该方法的局部变量和运行时数据。 -
在栈帧中,会分配空间用于存储
a
和b
两个参数,在这个例子中它们分别是3和5。 -
方法执行过程中,
sum
变量的值也会被存储在该栈帧中。 -
当
addNumbers
方法执行结束后,对应的栈帧会被弹出,栈的状态回到调用该方法的地方,同时将sum
的值作为返回值传递给调用方。
因此我们获得两个局部变量表,一个属于main一个属于addNumbers方法
main方法:
局部变量表:
args: 参数,这里是String数组,但在main方法中未使用。
result: 存储addNumbers方法返回的结果。
slot的使用:
args占用一个slot。
result占用一个slot。
执行过程:
addNumbers(3, 5) 方法调用时,传递参数3和5,result将存储addNumbers的返回值。
addNumbers方法:
局部变量表:
a: 参数,存储调用时传递的第一个参数。
b: 参数,存储调用时传递的第二个参数。
sum: 存储a和b的和。
slot的使用:
a占用一个slot。
b占用一个slot。
sum占用一个slot。
执行过程:
int sum = a + b; 执行时,将a和b相加的结果存储在sum中。
return sum; 返回sum的值。
这个过程中,虚拟机栈起到了存储方法调用信息、局部变量和返回值的作用,每个方法的执行都会在虚拟机栈中留下相应的痕迹,保证方法的调用和执行能够顺利进行。
3. 本地方法栈
- Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧
- native常用C、C++、汇编来实现,提供了直接访问内存和硬件的能力,在多线程的实现中,大量使用了native方法协助
- 在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
4. 堆内存(Java Heap
(听着就很大啊)
Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。
虚拟机对堆 的划分
Java虚拟机对堆的划分通常包括年轻代(Young Generation)、老年代(Old Generation)和永久代/元空间(Permanent Generation/Metaspace)等不同的区域。
1. 年轻代(Young Generation):
- 作用: 年轻代是新创建的对象的主要存放区域,大部分的对象都是朝生夕死的,因此这部分区域的垃圾回收频繁。
- 划分: 年轻代通常被划分为三个部分:Eden区(新生代对象的初始分配区域)和两个Survivor区(S0和S1,用于存放从Eden区复制过来的存活对象)。
- 对象的生成和晋升: 新创建的对象首先被分配到Eden区,经过垃圾收集后,存活的对象会被移到Survivor区,多次存活的对象最终会晋升到老年代。
2. 老年代(Old Generation):
- 作用: 老年代用于存放长期存活的对象,
通常是由年轻代中存活时间较长的对象晋升而来。
- 对象的晋升: 经过多次年轻代的垃圾收集后,仍然存活的对象会被晋升到老年代。
3. 永久代/元空间(Permanent Generation/Metaspace):
- 作用: 用于存放类的元信息、静态变量、常量等,不同虚拟机实现中可能存在差异。
- 替代关系: 在Java 8 及之后的版本,**永久代被元空间(Metaspace)**所取代。Metaspace不再位于堆内,而是位于本地内存,因此不再受到堆大小的限制。
注意事项:
- 内存管理: 堆内存的管理主要由垃圾收集器负责,不同的垃圾收集器有不同的算法和策略。
- 配置调优: 开发人员可以通过JVM的启动参数来调整堆的大小,例如使用
-Xmx
和-Xms
来设置最大堆大小和初始堆大小。 - OutOfMemoryError: 如果堆内存不足,可能会导致OutOfMemoryError异常,开发人员需要通过调整堆大小或优化程序来解决这类问题。
对象在堆中的生命周期
`对象在Java堆中的生命周期通常经历以下阶段:
1. 创建阶段(Creation):
- 对象通过
new
关键字在堆上进行分配空间,此时对象进入了堆中。
MyObject obj = new MyObject();
2. 引用阶段(Reference):
- 对象被引用,可以通过引用变量访问到对象。
关于引用是什么:
https://blog.csdn.net/m0_51663233/article/details/133755553
MyObject anotherObj = obj;
3. 使用阶段(Usage):
- 对象被程序使用,成为程序逻辑的一部分,进行各种操作。
int result = obj.calculateResult();
4. 不可达阶段(Unreachable):
- 对象不再被任何引用变量所引用,成为不可达对象。
- Java垃圾收集器通过标记-清除、标记-整理等算法,识别并清理不可达对象。
obj = null; // 不再引用原对象
5. 垃圾收集阶段(Garbage Collection):
- 垃圾收集器在堆中标记并清理不可达对象。
- 清理后,堆中的空间被释放,用于存放新的对象。
注意事项:
- 引用关系: 对象的生命周期与其引用关系密切相关。只有当对象不再被引用时,它才能成为垃圾收集的目标。
- 垃圾收集策略: Java虚拟机的垃圾收集器采用不同的策略来管理堆内存,如分代垃圾收集等。
- 内存泄漏: 如果对象在不再使用时没有被正确释放,可能导致内存泄漏问题,即堆中的空间不断被占用而无法回收。
对象的生命周期管理主要由Java虚拟机的垃圾收集器负责,确保不再被引用的对象能够被及时释放,从而保持堆内存的有效利用。
5. 方法区(Method Area
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
主要存储
在Java 8及之前版本,方法区通常包括永久代(PermGen);而在Java 8之后,永久代被元空间(Metaspace)所取代。
可以发现,方法区都是存的一些持久化的东修
以下是Java方法区的主要元素:
-
类的元数据信息: 包括类的结构、方法、字段、接口等信息。
-
常量池(Constant Pool): 存储编译期生成的各种字面量和符号引用。
-
静态变量(Static Variables): 存储类级别的静态变量。
-
运行时常量池: 是常量池的一部分,包含在类加载后进入方法区的。
-
即时编译器编译后的代码: 存储已被即时编译器(如HotSpot的C1和C2编译器)编译后的本地机器代码。