我们在之前几篇文章中,提到了类加载器和双亲委派机制,那么今天我们来说一下今天的知识点:运行时数据区。
运行时数据区:jvm的内存被划分成不同的区域,每个区域用于存储不同类型的数据,例如栈、堆、程序计数器等。
当Java程序被启动时,jvm会负责在内存中分配一些空间来存储程序执行时所需要的各种数据。这些空间被划分成几个不同的区域,每个区域用于存储不同类型的数据。常见的运行时数据区包括以下几个:
-
程序计数器(PC):程序计数器是一个较小的内存区域,它用于保存当前线程正在执行的字节码指令的地址。Java虚拟机只支持线程级别的程序计数器,因此每个线程都有自己独立的程序计数器。程序计数器是线程私有的,它的作用是记录当前执行的指令的地址或者下一条要执行的指令的地址。当线程执行Java方法、本地方法或者执行CPU的指令时,都需要进行指令跳转,其中程序计数器就是记录这个跳转位置的工具。
-
Java虚拟机栈:Java虚拟机栈是一个线程私有的内存区域,它用于存储线程执行方法时的局部变量、操作数栈、方法出口等信息。每个方法执行时都会在虚拟机栈中创建一个栈帧,用于存储方法的参数、局部变量等信息。如果虚拟机栈内存溢出,将会抛出StackOverflowError异常;如果无法申请到足够的空间来存储新的栈帧,将会抛出OutOfMemoryError异常。
-
堆:堆是Java程序中最大的一块内存区域,被所有线程共享。它用于存储Java对象和数组。在不同的JVM实现中,堆的内存可以动态进行分配和释放。由于堆内存是被所有线程所共享的,因此它容易成为内存泄漏和GC的瓶颈。Java堆是由垃圾回收器管理的,因此也被称作垃圾回收堆。
-
方法区:方法区也被称为永久代,在Java 8之后被替换成了元空间。方法区用于存储JVM加载的类信息、常量池等。方法区也是所有线程共享的内存区域,并且它的大小可以动态调整。如果方法区内存溢出,将会抛出OutOfMemoryError异常。
-
运行时常量池:运行时常量池是在JVM加载每个类时创建的,用于存储该类中的常量池信息。在类的字节码文件中,常量池就是存在这个文件中的,而在JVM内存中,会将这些信息保存到运行时常量池中。运行时常量池中存储的内容有:字符串字面值、final常量和static final常量的值、类和接口的全限定名、字段描述符、方法描述符、方法名和方法类型以及ldc指令所表示的常量值。
除了上述五个运行时数据区之外,Java虚拟机还有下面几个内存区域:
-
直接内存(Direct Memory):直接内存并不是JVM运行时数据区的一部分,但是JVM在管理直接内存时也需要扮演一定的角色。直接内存并不是用JVM堆内存管理机制来管理的,而是通过使用Native函数库直接向操作系统申请的内存,因此它可以避免在Java堆和Native堆之间进行复制数据的过程,提高了程序的执行效率。同样,直接内存的使用也需要更多的谨慎,一旦超过物理内存限制,也会抛出OutOfMemoryError异常。
-
本地方法栈:本地方法栈和虚拟机栈类似,只不过它为Native方法服务,其中保存的是Native方法的参数和局部变量等信息。和虚拟机栈一样,本地方法栈也是由操作系统分配的。如果本地方法栈内存溢出,则会抛出StackOverflowError或OutOfMemoryError异常。
-
PC寄存器:PC寄存器是线程私有的内存区域,也被称为当前线程的“程序计数器”,用于记录线程执行字节码指令的地址。每当线程执行一个指令时,PC寄存器的值就会被更新。如果当前线程执行的是Java方法,那么PC寄存器中存储的就是该方法的指令地址;如果当前线程执行的是Native方法,那么PC寄存器的值则为undefined。
-
元空间(Metaspace):元空间是JVM 8之后取代了方法区的一种内存区域,用于存储类的元数据信息,包括类名、接口、变量、方法等。元空间和方法区不同,使用的是本地内存,而不是JVM的堆内存。在默认情况下,元空间的大小是不受限制的,但是需要根据实际使用情况动态进行调整。如果元空间内存溢出,则会抛出OutOfMemoryError异常。
我们可以编写一些测试案例来验证JVM运行时数据区的设计和分配情况。
- Java堆的测试案例
Java堆是JVM管理内存空间中最大的一部分,我们可以通过创建大量的对象来测试Java堆的分配情况。以下是一个简单的Java程序,用于连续创建指定数量的对象,并在创建对象后检查当前堆内存使用情况:
public class HeapTest {
static int count = 0;
public static void main(String[] args) throws Exception {
while (true) {
byte[] b = new byte[1024 * 1024];
count++;
System.out.println("已创建了 " + count + " 个对象");
Thread.sleep(1000);
}
}
}
该程序会连续创建1MB大小的byte数组对象,并每隔一秒打印创建对象的数量。我们可以使用jvisualvm
等工具来观察Java堆内存的分配和堆内存的使用情况。当Java堆内存超出设置的阈值时,就会抛出OutOfMemoryError异常。
2.方法区/元空间的测试案例
方法区/元空间主要用于存放类信息,当系统加载大量类文件时,方法区/元空间的内存会被不断占用。我们可以通过下面这个测试程序来验证方法区/元空间的内存分配情况:
import java.util.ArrayList;
import java.util.List;
public class MethodAreaTest {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
}
}
该程序会连续创建对象并将其添加到List
中,导致不断地加载类信息,从而消耗方法区/元空间的内存。我们可以使用jstat
等工具来观察方法区/元空间的内存使用情况。当方法区/元空间的内存超出设置的阈值时,就会抛出OutOfMemoryError异常。
3.Java虚拟机栈和本地方法栈的测试案例
Java虚拟机栈和本地方法栈主要用于保存线程执行方法的信息,我们可以通过以下程序来验证Java虚拟机栈和本地方法栈的内存分配情况:
public class StackTest {
static int count = 0;
public static void main(String[] args) {
try {
recursive();
} catch (Throwable t) {
System.out.println("栈深度为:" + count);
t.printStackTrace();
}
}
private static void recursive() {
++count;
recursive();
}
}
该程序会经过递归调用,不断增加栈帧的深度,并打印当前栈帧深度。我们可以使用-Xss
参数指定栈的大小(默认为1MB),以观察Java虚拟机栈和本地方法栈的内存使用情况。当Java虚拟机栈和本地方法栈的内存超出设置的阈值时,就会抛出StackOverflowError或OutOfMemoryError异常。注意:以上测试程序会不断占用内存并导致OOM异常,请谨慎使用。
那么什么是OOM异常呢?
OOM指OutOfMemoryError,是Java中常见的运行时异常之一。当JVM内存不足以分配新的对象或执行新的方法时就会抛出OOM异常。
通常情况下,当我们创建过多的对象或者使用递归等方式导致栈深度太大时,就容易出现OOM异常。另外,如果使用的是虚拟机内存默认值(如-Xmx、-Xms等),可能会导致内存不足而抛出OOM异常。
JVM的OOM异常是一种比较严重的异常,因为OOM异常发生后程序无法正常运行,需要及时采取措施来解决问题。通常情况下,我们需要对应用进行优化,修改代码逻辑,或调整JVM的内存配置参数等来预防OOM异常的发生。
如下图添加 VM options:-Xms1024m -Xmx1024 -XX:+PrintGCDetails