JVM的内存结构在JDK1.8有所改变,本文简述JDK1.8的内存结构。

在JDK1.8中,JVM的内存结构主要分为两大块:
一、线程共享
根据数据区域的不同,可分为两部分:
1.运行时数据区域
-
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,“几乎”所有的对象实例以及数组都在这里分配内存。
-
运行时常量池
字符串常量池位于堆中(JDK1.7及之后),是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 创建字符串对象”ab“,会在堆中将字符串对象”ab“的引用保存在字符串常量池中 String aa = "ab"; // 直接返回字符串常量池中字符串对象”ab“的引用 String bb = "ab"; System.out.println(aa==bb);// true,这里比较的是地址而不是值,即代表两者指向同一对象
-
Java 6及之前的版本中,字符串常量池是存放在方法区(永久代)中的。
-
Java 7开始,字符串常量池被移到了堆中。
-
Java 8及以后,永久代被元空间取代,运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。
-
-
2.本地内存
①方法区(元空间)
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。但是,《Java 虚拟机规范》只规定了方法区的概念及其作用,并没有规定如何实现。因此,方法区在不同的虚拟机实现上的实现是不同的。在HotSpot虚拟机中,方法区的实现是元空间(JDK1.8及之后)。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区和元空间是什么关系呢?
方法区和元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说元空间是 HotSpot 虚拟机对虚拟机规范中方法区的实现方式。

除此之外,Class文件中还有用于存放编译期生成的各种**字面量(Literal)和符号引用(Symbolic Reference)**的 常量池表(Constant Pool Table) 。
**字面量:**是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。
**符号引用:**常见的符号引用包括 类符号引用、字段符号引用、方法符号引用、接口方法符号。
常量池表会在类加载后存放到方法区的运行时常量池中。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError
错误。
②直接内存
JDK1.4 中新加入的 NIO(Non-Blocking I/O,也被称为 New I/O),引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI(Java Native Interface,Java本地接口)的方式在本地内存上分配的。直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError
错误出现。
二、线程私有
线程私有的部分均在运行时数据区域中,且这一部分是在线程中的(图中黄色部分):

1.虚拟机栈
Java 虚拟机栈**(下称“栈”)**也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

①局部变量表
主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
②操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
③动态链接
主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用(比如方法引用的符号引用)。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
④方法返回地址
存放调用该方法的PC寄存器的值,用以在该方法结束后返回到上一级方法。
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
2.本地方法栈
本地方法栈与虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError
和 OutOfMemoryError
两种错误。
3.程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。其主要作用有两个:
- 字节码解释器通过改变程序计数器来选取下一条需要执行的字节码指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
⚠️ 注意:程序计数器是唯一一个不会出现 OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。