Java虚拟机(JVM)的内存模型定义了Java程序在运行时如何使用内存。它主要分为以下几个部分:
-
程序计数器(Program Counter Register):
- 每个线程都有一个程序计数器,用于指示当前正在执行的指令的地址或索引。在多线程环境下,每个线程都有自己的程序计数器,互不干扰。
-
Java虚拟机栈(Java Virtual Machine Stacks):
- 每个线程都有一个虚拟机栈,用于存储局部变量、方法参数、方法返回值以及部分运算结果。每个方法调用都会创建一个栈帧,栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。
-
本地方法栈(Native Method Stack):
- 与虚拟机栈类似,但是用于执行本地(Native)方法。
-
Java堆(Java Heap):
- Java堆是Java虚拟机中最大的一块内存区域,用于存储对象实例和数组。在堆中分配的对象可以由垃圾回收器进行管理,通过自动内存管理机制实现内存的分配和释放。
-
方法区(Method Area):
- 方法区用于存储类的结构信息、静态变量、常量、方法字节码等数据。在HotSpot虚拟机中,方法区被称为“永久代”,但在Java 8之后被替换为“元空间(Metaspace)”。
-
运行时常量池(Runtime Constant Pool):
- 运行时常量池是方法区的一部分,用于存储编译期生成的字面量和符号引用。在运行时,这些常量池会被加载到方法区中。
-
直接内存(Direct Memory):
- 直接内存并不是Java虚拟机规范中定义的内存区域,但是在Java NIO中被广泛使用。直接内存通常是通过Native方法分配的,它可以在Java堆之外进行直接分配和释放内存,提高I/O操作的效率。
1、程序计数器
程序计数器(Program Counter Register)是Java虚拟机中的一块内存区域,用于指示当前线程执行的字节码指令地址。在Java虚拟机中,每个线程都有自己的程序计数器。
主要作用包括:
-
线程私有:程序计数器是线程私有的,每个线程都有自己的程序计数器。这样可以保证在多线程环境中,每个线程都能独立地执行字节码指令。
-
线程切换时保存状态:当线程切换时,程序计数器会保存当前线程的执行状态,以便后续线程恢复执行时能够继续执行正确的字节码指令。
-
指令地址记录:程序计数器中保存了当前线程正在执行的字节码指令的地址,也就是下一条将要执行的指令的位置。
-
支持线程恢复执行:程序计数器的值可以被JVM用来支持线程恢复执行。当一个线程被挂起或者等待唤醒时,JVM可以根据程序计数器的值来确定下一条要执行的指令,从而在线程重新执行时继续执行正确的指令。
-
线程执行控制:程序计数器可以用于控制线程的执行流程,例如循环、分支、跳转等控制结构。
需要注意的是,程序计数器是Java虚拟机中唯一一个在多线程环境下不会出现内存溢出(OutOfMemoryError)的区域,因为它是线程私有的,每个线程都有自己独立的程序计数器。
2、Java虚拟机栈(Java Virtual Machine Stacks)
Java虚拟机栈(Java Virtual Machine Stacks)是Java虚拟机中的一块内存区域,用于存储线程的方法调用栈帧(Stack Frame)。每个线程在执行Java方法时都会创建一个对应的栈帧,栈帧包含了方法的局部变量、操作数栈、动态链接、方法返回地址等信息。
主要作用包括:
-
方法调用:Java虚拟机栈用于存储方法调用的信息。每当一个方法被调用时,就会创建一个新的栈帧,并将其压入栈顶。当方法执行完毕后,栈帧会被弹出,控制权会返回给调用该方法的地方。
-
局部变量:栈帧中包含了方法的局部变量表,用于存储方法的参数和局部变量。局部变量表中的数据只在方法的执行期间有效,方法执行完毕后就会被销毁。
-
操作数栈:栈帧中还包含了一个操作数栈,用于存储方法执行过程中的临时数据和计算结果。操作数栈是一个后进先出(LIFO)的数据结构,用于支持方法中的运算操作。
-
方法返回:栈帧中还包含了方法的返回地址,用于在方法执行完毕后返回到方法调用的地方。当方法执行完成时,栈帧会被弹出,并将控制权返回给调用该方法的地方。
-
异常处理:Java虚拟机栈也用于异常处理。当方法执行过程中发生异常时,栈帧中会包含异常处理器的信息,用于查找并执行适当的异常处理代码。
需要注意的是,Java虚拟机栈的大小是固定的,并且在虚拟机启动时就会被分配好。如果栈空间不足,将会抛出StackOverflowError;如果栈无法扩展,将会抛出OutOfMemoryError。
3、本地方法栈(Native Method Stack)
本地方法栈(Native Method Stack)与虚拟机栈类似,但是用于执行本地方法(Native Method)时使用。在Java程序中,本地方法是用本地语言(如C或C++)编写的方法,它们通过Java本地接口(JNI)与Java程序进行交互。
主要作用包括:
-
本地方法调用:本地方法栈用于存储本地方法的调用信息。当Java程序调用本地方法时,会创建一个新的栈帧,并将其压入本地方法栈顶。当本地方法执行完毕后,栈帧会被弹出,控制权会返回给Java程序。
-
本地方法参数:本地方法栈中包含了本地方法的参数和局部变量。与虚拟机栈类似,本地方法栈也包含了一个局部变量表,用于存储方法的参数和局部变量。
-
本地方法返回:本地方法栈中也包含了本地方法的返回地址,用于在方法执行完毕后返回到Java程序调用的地方。
需要注意的是,本地方法栈与虚拟机栈是相互独立的,它们分别用于执行Java方法和本地方法。本地方法栈的大小也是固定的,并且在虚拟机启动时就会被分配好。如果本地方法栈空间不足,将会抛出StackOverflowError;如果本地方法栈无法扩展,将会抛出OutOfMemoryError。
4、Java堆(Java Heap)
Java堆(Java Heap)是Java虚拟机中最大的一块内存区域,用于存储对象实例和数组。在Java程序中,几乎所有的对象都会被分配在堆上。
主要特点包括:
-
对象存储:Java堆用于存储Java程序中创建的对象实例。当使用关键字
new
创建对象时,对象的内存空间就会被分配在堆上。 -
动态分配:Java堆是动态分配的,可以根据程序的需要动态地分配和回收内存空间。当创建对象时,Java堆会动态分配一块内存空间来存储对象;当对象不再被引用时,Java堆会自动回收这些内存空间,以便下次分配给新的对象。
-
堆内存管理:Java堆的内存管理由Java虚拟机的垃圾回收器(Garbage Collector)来负责。垃圾回收器会定期扫描堆内存中的对象,找出不再被引用的对象,并将其回收释放,以便下次分配给新的对象。
-
堆大小限制:Java堆的大小可以通过启动参数来配置。如果堆空间不足,将会抛出OutOfMemoryError异常。
-
分代结构:Java堆通常被划分为新生代(Young Generation)、老年代(Old Generation)和永久代(PermGen)等不同的区域,不同区域的对象分配和回收策略不同。Java 8后永久代被元空间(Metaspace)取代。
Java堆的大小对程序的性能和内存使用有很大影响,合理配置堆大小可以提高程序的性能和稳定性。
5、方法区(Method Area)
方法区(Method Area)是Java虚拟机中的一块内存区域,用于存储类的结构信息、静态变量、常量、方法字节码等数据。在HotSpot虚拟机中,方法区被称为“永久代(PermGen)”,但在Java 8之后被替换为“元空间(Metaspace)”。
主要特点包括:
-
类信息存储:方法区用于存储类的结构信息,包括类的成员变量、方法、构造器等信息。每个类在内存中都会有一个对应的Class对象,Class对象包含了类的结构信息。
-
静态变量:方法区用于存储类的静态变量,静态变量在类加载时就会被分配内存空间,并且在整个程序的生命周期内都存在。
-
常量池:方法区还包含了类的常量池,用于存储编译期生成的字面量和符号引用。常量池中的数据在类加载时就会被加载到方法区中,并且可以被整个程序访问。
-
方法字节码:方法区还用于存储方法的字节码,即编译后的机器码。当程序调用一个方法时,虚拟机会在方法区中找到对应的字节码并执行。
-
永久代与元空间:在Java 8之前,方法区被实现为永久代(PermGen),但是永久代有一些限制和问题,比如内存泄漏和性能问题。因此,在Java 8中,永久代被元空间(Metaspace)取代。元空间使用本地内存来存储类的元数据,可以根据需要动态分配和释放内存,避免了永久代的一些问题。
方法区的大小可以通过启动参数来配置,如果方法区空间不足,将会抛出OutOfMemoryError异常。需要注意的是,在Java 8及更高版本中,由于永久代被元空间取代,因此调整方法区大小的方式和参数也会有所不同。
6、运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期生成的字面量和符号引用,以及一些运行期间生成的常量。
主要特点包括:
-
存储常量:运行时常量池用于存储类文件中的常量,包括字符串常量、基本数据类型常量、类和接口的全限定名、字段和方法的符号引用等。
-
动态生成:除了编译期生成的常量外,运行时常量池还可以在运行时动态生成一些常量。比如,通过调用String类的intern()方法可以在运行时将字符串常量添加到常量池中。
-
共享常量池:运行时常量池是类加载时从.class文件中加载到方法区的一部分数据,每个类都有自己的运行时常量池。在虚拟机中,常量池是共享的,即一个类的常量池中的常量可以被其他类访问和共享。
-
字符串常量池:字符串常量池是运行时常量池中的一部分,用于存储字符串常量。Java中的字符串常量池是特殊的,当使用字符串字面量(如"abc")创建字符串对象时,如果常量池中已经存在相同的字符串常量,则会直接引用已有的常量;如果常量池中不存在相同的字符串常量,则会创建一个新的字符串常量并添加到常量池中。
-
动态性:运行时常量池具有一定的动态性,可以在运行时动态地向其中添加常量。这使得一些特定的场景下可以实现动态生成常量的功能。
运行时常量池是方法区的一部分,因此它的大小受到方法区大小的限制。如果运行时常量池空间不足,将会抛出OutOfMemoryError异常。
7、直接内存(Direct Memory)
直接内存(Direct Memory)是Java虚拟机中的一种特殊内存,它并不是Java堆中的一部分,而是在堆外直接分配的内存。直接内存的分配和释放不受Java堆大小的限制,通常通过本地方法(Native Method)来分配和释放。
主要特点包括:
-
使用ByteBuffer分配:在Java中,直接内存通常是通过ByteBuffer类的allocateDirect()方法来分配的。这个方法会在堆外直接分配一块内存空间,并返回一个ByteBuffer对象来操作这块内存。
-
底层操作系统内存:直接内存的分配和释放通常是通过本地方法(Native Method)来操作底层操作系统的内存,而不是通过Java虚拟机的内存管理机制来操作。这样可以避免了Java堆中的垃圾回收机制对直接内存的影响。
-
提高I/O效率:直接内存通常用于高性能的I/O操作,比如NIO中的通道(Channel)和缓冲区(Buffer)。通过直接内存,可以避免了Java堆和本地操作系统之间的数据复制,提高了I/O操作的效率。
-
非受管理内存:直接内存是非受管理的,即Java虚拟机不会对直接内存进行垃圾回收。因此,需要手动释放直接内存以防止内存泄漏。
需要注意的是,直接内存虽然提高了一些特定场景下的性能,但是它的分配和释放通常比Java堆中的内存更加昂贵。因此,只有在需要高性能的I/O操作时才建议使用直接内存。