运行时数据区是JVM用来管理和存储Java程序执行过程中所需要的各种数据的区域,也成为JVM内存结构。JVM内存结构主要包括以下几个部分:
- 程序计数器(Program Counter Register)
程序计数器是一种特殊的内存区域,它可以在物理机器的寄存器中实现,也可以在虚拟机栈中实现。程序计数器是JVM中的一个非常重要的组成部分,它用于存储当前线程所执行的字节码指令的地址。当JVM在执行Java方法时,程序计数器用于记录当前执行的位置,以便下一次继续执行。
程序计数器是线程私有的,每个线程都有一个独立的程序计数器,因此可以避免多线程之间的干扰和竞争。程序计数器的容量比较小,一般为32位或64位,它的值存储在线程私有的虚拟机栈中。
- Java虚拟机栈(Java Virtual Machine Stacks)
栈内存是用于存储栈帧的内存空间,栈帧中包含操作数栈、方法出口和局部变量表等信息。每个线程都有自己的栈内存,栈内存的大小是固定的(默认1MB),在编译时就已经确定,且一旦分配无法改变。当一个方法被调用时,JVM会为该方法分配一块内存空间(栈帧),方法执行结束后,该空间会被释放。由于栈内存是线程私有的,因此不会出现线程安全问题。
局部变量表中存储方法内部声明的变量,操作数栈中存储参与运算或者赋值操作的值,可以是各种类型的数据,例如整型、浮点型、对象引用等。方法出口指的是返回地址,在一个方法执行完毕后需要返回到哪个方法继续执行的地址。
public class JVMStacksDemo {
static void methodA() {
int a = 0;
int b = 10;
methodB(a, b);
}
static int methodB(int a, int b) {
int x = 100;
int c = a + b + x;
String str = "str";
return c;
}
public static void main(String[] args) {
int a = 20;
int b = 20;
methodA();
}
}
当main
方法被调用时,JVM会为其创建一个栈帧,并将该方法的返回地址压入栈顶。该栈帧中包含了main
方法的局部变量表和操作数栈。main
方法中定义了两个整型变量a
、b
,并将a
和b
分别赋值为20
。如下图,可以看到当前栈中有main
方法的栈帧,且可以看到右侧局部变量表中包含main
方法的命令行参数、定义的整型变量a
、b
以及它们的值。
接着main
方法调用methodA
。
此时,JVM会为methodA
方法创建一个新的栈帧,并将该方法的返回地址压入栈顶。如下图可以看到栈中methodA
的栈帧位于栈的顶部。
在methodA
中调用的methodB
,在methodB的局部变量表中可以看到methodA
传入的参数a
、b
,定义的变量x
、c
,引用数据类型变量str
。
当methodB执行完成后,methodB的栈帧出栈。methodA同理。最后main方法执行完毕,栈空间被释放。
- Java堆(Java Heap)
堆内存是Java程序中最大的一块内存,用于存储对象实例和数组。
在JVM启动时,会创建并分配一个初始大小的堆空间,这个大小可以通过JVM参数来指定。当堆空间不足时,JVM会自动扩展堆的大小。堆内存被所有线程共享,因此可能会出现线程安全问题。但通常情况下,Java对象的访问都是通过引用来进行的,因此不需要考虑对象本身的线程安全问题,而是需要考虑对引用的并发访问问题。
-
堆内存存储的对象实例和数组都需要使用new关键字创建,如果没有使用new关键字,则存储在栈内存中。
-
对象实例包括String、ArrayList、HashMap等java中的,也包括自定义的。
-
堆内存还存储对象的实例变量,如Person类的name、age等实例变量。类的静态变量存储在方法区中,它们也是所有线程共享的。
-
一些基本类型的包装类也是存储在堆内存中,如常用的包装类Integer、Long、Float、Double、Boolean、Byte、Short和Character等。
堆内存在逻辑上划分为新生代和老年代,新生代又分为Eden区、Survivor1区和Survivor2区。新生代主要存储生命周期较短的对象,而老年代则主要存储生命周期较长的对象。
当一个Java对象被创建时,它首先会被分配在Eden区。当Eden区满时,触发Minor GC垃圾回收,将仍然存活的对象复制到Survivor0区或Survivor1区中。当Survivor0区或Survivor1区满时,也会触发Minor GC,将仍然存活的对象复制到另一个Survivor区中。在经过多次GC后,仍然存活的对象会被移动到老年代中。
- 方法区(Method Area)
方法区是用于存储被加载的类的相关信息(包括类名、访问修饰符、常量池、字段描述、方法描述等)以及常量池中的字面量、符号引用等内容的内存空间。方法区在逻辑上是堆的一部分。
-
方法区被所有线程共享,在JVM启动时被创建,JVM销毁时被销毁。JVM会根据实际情况自行调整方法区的大小。
-
常量池、静态变量在jdk8之后,常量池和静态变量被转移到了元空间(Metaspace)中。
-
方法区也是垃圾回收的目标之一,主要回收无用的类信息和常量池中的常量。
- 本地方法栈(Native Method Stacks)
本地方法是使用 C/C++ 或者其他本地语言编写的方法,它们不会像 Java 方法那样被编译成字节码,而是被编译成本地机器码。本地方法栈和栈内存类似,用于存储本地方法的调用栈和本地方法的局部变量。本地方法栈也是线程私有的。在执行本地方法时,JVM 将会在本地方法栈中创建一个栈帧,用来存储本地方法的参数和局部变量。和 Java 方法栈类似,栈帧由操作数栈和局部变量表组成,它们的作用也和 Java 方法栈一样。