讲讲 JVM 的内存结构
什么是 JVM 内存结构?
JVM内存结构分为5大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区。
HotSpot在JDK1.8之前方法区就是永久代,永久代就是方法区。JDK1.8后删除了永久代,改为元空间,元空间在本地内存中。方法区就是元空间,元空间就是方法区。
创建一个线程,JVM就会为其分配一个私有内存空间,其中包括PC、虚拟机栈和本地方法栈
简单来说:
- 堆:存放 new 出来的东西
- 方法区:被虚拟机加载的类信息、常量、静态常量等。
- 栈:存放局部变量
- 程序计数器:记录指令
- 本地方法栈:Native 方法
接着进行详细的介绍:
线程私有
程序计数器
线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:
- 当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制 ,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况 下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。
程序计数器是唯一一个不会出现 OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。
局部变量表是用于存放方法参数和方法内的局部变量。
每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。
- 部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接
- 部分符号引用在运行期间转化为直接引用,这种转化就是动态链接
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
Java 虚拟机栈会出现两种错误:StackOverFlowError
和 OutOfMemoryError
。
可以通过 -Xss
参数来指定每个线程的虚拟机栈内存大小:java -Xss2M
本地方法栈
虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Native 方法一般是用其它语言(C、C++等)编写的。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
线程共享
堆
堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。
堆可以细分为:新生代(Eden空间(伊甸园)、From Survivor、To Survivor空间)和老年代。
通过 -Xms
设定程序启动时占用内存大小,通过 -Xmx
设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory
异常。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。
注意
永久代
方法区是 JVM 的规范,而永久代PermGen 是方法区的一种实现方式,并且只有 HotSpot 有永久代。对于其他类型的虚拟机,如 JRockit 没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。
元空间
JDK 1.8 的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
两者最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
为什么要将永久代替换为元空间呢?
永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。
运行时常量池
运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 错误出现。
NIO 的 Buffer 提供了 DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。
DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。
直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。
代码详解
以下是一个简单的 Java 类,我们将在注释中说明各个内存区域的作用:
java
public class MemoryStructureExample {
// 静态变量,存放在方法区
private static int staticVar = 42;
public static void main(String[] args) {
// 局部变量,存放在虚拟机栈
int localVar = 10;
// 创建一个对象,存放在堆中
MemoryStructureExample obj = new MemoryStructureExample();
// 调用方法,创建一个新的栈帧
obj.doSomething(localVar);
// 创建一个数组,存放在堆中
int[] array = new int[5];
// 常量,存放在方法区
final String constantString = "Hello, world!";
// 本地方法调用,使用本地方法栈
System.out.println("Static variable: " + staticVar);
System.out.println("Constant string: " + constantString);
}
// 方法,存放在方法区
public void doSomething(int value) {
// 局部变量,存放在虚拟机栈
int result = value * 2;
System.out.println("Result: " + result);
}
}
在这段代码中,我们可以看到:
staticVar
是一个静态变量,存放在方法区。localVar
是一个局部变量,存放在虚拟机栈。obj
是一个对象,存放在堆中。array
是一个数组,也存放在堆中。constantString
是一个常量,存放在方法区。doSomething
方法创建了一个新的栈帧,其中的局部变量存放在虚拟机栈。
总之,这些内存区域共同构成了 Java 虚拟机的内存结构,确保 Java 程序的正常运行和优化。如果你还有其他问题,欢迎继续提问!