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):
运行时数据区域(Runtime Data Area):
执行引擎(Execution Engine):
本地方法接口(Native Interface):
本地方法栈(Native Method Stack):
在整个加载字节码文件的流程中,类加载器负责加载和验证字节码文件,运行时数据区域存储了加载的类信息和对象实例,执行引擎执行字节码指令,本地接口实现 Java 与本地方法的交互,本地方法栈用于执行本地方法。这些组成部分相互协作,使得 JVM 能够正确加载和执行字节码文件。
接下来我们对内存区域进行剖析
虚拟机中的一块内存空间,它是线程私有的,也即每个线程都有自己独立的程序计数器。程序计数器可以看作是当前线程所执行的字节码的行号指示器,即它指向当前线程正在执行的字节码指令的地址。在Java虚拟机规范中,程序计数器是唯一一块在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
在程序计数器中,存储着当前线程正在执行的 Java 字节码指令的地址,或者正在执行的本地方法的指令地址。由于程序计数器是线程私有的,因此它不会发生线程切换时的数据同步问题。
程序计数器在线程切换、线程恢复以及指令重复执行等方面发挥着重要作用。在多线程环境下,程序计数器能够确保线程在切换后能够正确恢复到之前的执行位置,从而保证了程序的正常执行。
与操作系统的一些区别:
用于存储线程的方法调用和局部变量。每个线程在创建时都会被分配一个对应的虚拟机栈,用于跟踪线程的方法调用和执行情况。
开始分析前,还需要了解一些概念
我们再对栈帧具体探索:
栈帧(Stack Frame)是虚拟机栈中的一个重要概念,它包含了方法在执行过程中所需的各种信息。以下是栈帧通常包含的信息:
局部变量表(Local Variable Table):用于存储方法中的局部变量,包括方法参数、方法内部定义的变量等。局部变量表中的每个元素都可以存储一个基本数据类型或者一个对对象的引用。
操作数栈(Operand Stack):操作数栈用于存储方法执行过程中的操作数,例如进行算术运算时需要使用的数值。方法执行时会从局部变量表中获取数据,进行计算后将结果存入操作数栈中。
动态链接(Dynamic Linking):指向运行时常量池中该方法的引用,通过动态链接可以在运行时解析调用的方法、字段等。
返回地址(Return Address):记录了方法调用结束后需要返回的指令地址,用于恢复到方法调用点继续执行。
附加信息:栈帧中还可能包含一些额外的附加信息,例如异常处理相关的信息、调试信息等。
方法调用: 当一个方法被调用时,一个新的栈帧被压入栈顶。这个栈帧包含了方法的参数、局部变量以及用于存储中间计算结果的操作数栈。
栈的压栈和弹栈: 方法调用时,栈帧被压入栈顶;方法返回时,栈帧被弹出。这种后进先出(LIFO)的结构保证了方法的调用和返回的顺序。
局部变量表: 每个栈帧中包含一个局部变量表,用于存储方法中的局部变量。包括方法参数、方法内部定义的局部变量等。
当一个方法被执行时,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的值。
这个过程中,虚拟机栈起到了存储方法调用信息、局部变量和返回值的作用,每个方法的执行都会在虚拟机栈中留下相应的痕迹,保证方法的调用和执行能够顺利进行。
(听着就很大啊)
Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。
Java虚拟机对堆的划分通常包括年轻代(Young Generation)、老年代(Old Generation)和永久代/元空间(Permanent Generation/Metaspace)等不同的区域。
通常是由年轻代中存活时间较长的对象晋升而来。
-Xmx
和 -Xms
来设置最大堆大小和初始堆大小。`对象在Java堆中的生命周期通常经历以下阶段:
new
关键字在堆上进行分配空间,此时对象进入了堆中。MyObject obj = new MyObject();
MyObject anotherObj = obj;
int result = obj.calculateResult();
obj = null; // 不再引用原对象
对象的生命周期管理主要由Java虚拟机的垃圾收集器负责,确保不再被引用的对象能够被及时释放,从而保持堆内存的有效利用。
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
在Java 8及之前版本,方法区通常包括永久代(PermGen);而在Java 8之后,永久代被元空间(Metaspace)所取代。
可以发现,方法区都是存的一些持久化的东修
以下是Java方法区的主要元素:
类的元数据信息: 包括类的结构、方法、字段、接口等信息。
常量池(Constant Pool): 存储编译期生成的各种字面量和符号引用。
静态变量(Static Variables): 存储类级别的静态变量。
运行时常量池: 是常量池的一部分,包含在类加载后进入方法区的。
即时编译器编译后的代码: 存储已被即时编译器(如HotSpot的C1和C2编译器)编译后的本地机器代码。