概要
在 Java 8 中,虚拟机内存主要由以下几个部分组成:
程序计数器(Program Counter Register):用于保存当前线程执行的位置,可以看作是当前线程所执行的字节码的行号指示器,当线程被切换后,用来恢复线程执行的位置。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口(方法返回地址)等信息。每个线程都拥有一个虚拟机栈,它的生命周期与线程相同。
本地方法栈(Native Method Stack):它与虚拟机栈相似,只是为本地方法服务。
堆(Heap):它是 Java 虚拟机中内存最大的一块,用于存储对象实例。堆为所有线程共享,是非线程安全的。是垃圾收集器管理的主要区域,因此也被称为“GC 堆”。
- 字符串常量池(String Constant Pool):堆的一部分,专门存储字符串。
方法区(Method Area):它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JDK7 及之前,HotSpot 虚拟机通过永久代(PermGen)实现方法区,在 JDK8 之后,永久代被元空间(Meta Space)所取代。
- 运行时常量池(Runtime Constant Pool):方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
直接内存(Direct Memory):直接内存 (Direct Memory) 并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。在 JDK 1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道 (Channel) 与缓冲区 (Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

程序计数器(Program Counter Register)
Java 8 中的程序计数器是一块较小的内存区域,用于记录当前线程执行字节码的位置,也就是指向下一条将要执行的指令的地址。
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
程序计数器是线程私有的,也就是每个线程都有一个独立的计数器,程序计数器的值在线程之间是互相独立的,不会出现线程安全问题。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
另:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域
虚拟机栈(VM Stack)
每个线程在创建时,Java 虚拟机会为其分配一个独立的虚拟机栈(下文简称栈),线程内的方法调用是通过其虚拟机栈实现的。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
方法调用的数据需要通过栈进行传递。每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
在方法调用时,Java 虚拟机会将调用方法所需的参数、返回地址等信息压入该线程对应的虚拟机栈中,然后执行方法体中的代码。当方法执行完毕,返回时,Java 虚拟机会将之前压入虚拟机栈的信息弹出,并返回到调用该方法的位置。
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说,栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机所允许的最大深度的时候,就抛出 StackOverFlowError 错误。
除了 StackOverFlowError 错误之外,栈还可能会出现 OutOfMemoryError 错误,这是因为如果栈的内存大小可以动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
- StackOverFlowError:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError:如果栈的内存大小可以动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
HotSpot 虚拟机的栈容量是不可以动态扩展的,所以在 HotSpot 虚拟机上不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不会有 OOM,但是如果申请时就失败,仍然会出现 OOM。
以下是一个简单的示例代码,用于演示虚拟机栈的使用:
public class StackDemo {public static void main(String[] args) {int result = calculate(10);System.out.println("Result: " + result);}public static int calculate(int num) {if (num == 1) {return 1;}return num * calculate(num - 1);}
}
在上述代码中,calculate 方法使用了递归调用来计算 num 的阶乘。每次调用该方法时,都会将当前 num 的值及其他信息封装成栈帧压入虚拟机栈中,并在计算完下一次调用的结果后弹出。整个过程先会多次压入栈帧,直到 num 的值为 1 时,开始逐个弹出栈帧并将计算结果返回。
本地方法栈(Native Method Stack)
它为虚拟机使用到的本地方法提供栈支持,和虚拟机栈类似,区别是虚拟机栈为 Java 方法服务,本地方法栈为 Native 方法服务。栈的大小可以通过设置 -Xss 来控制,默认大小为 1MB。
《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,在 HotSpot 虚拟机中,直接就把本地方法栈和虚拟机栈合二为一。
堆(Heap)
堆(Heap)是 Java 虚拟机用于存放对象实例的区域,是 JVM 中最大的一块内存区域,可以通过设置 -Xmx 和 -Xms 来控制最大和最小值。堆内存是线程共享的,因此在多线程的情况下需要进行并发控制。
在 Java 程序中,每当使用 new 关键字创建一个对象时,JVM 会自动在堆内存中为该对象分配内存空间,并返回该对象的引用。堆是垃圾收集器所管理的内存区域,当堆中没有可用内存时,会抛出 OutOfMemoryError 异常。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
方法区(Method Area)
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。永久代空间不足会抛出 OutOfMemoryError 异常。
永久代(Permanent Generation)以及元空间(Metaspace)是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
元空间使用本地内存实现,不再像方法区那样有固定的大小限制,它的大小受限于操作系统的可用内存大小。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
例如,下面的代码中定义了一个类 Person,其中包含一个静态变量 count 和一个静态方法 getCount:
public class Person {public static int count = 0;public static int getCount() {return count;}
}
在这个例子中,当这个类被加载时,类的信息(包括静态变量 count 和静态方法 getCount)就会被存储在方法区/元空间中。每个线程都可以通过调用 Person.getCount() 方法来获取 count 的值,因为它是共享的。
运行时常量池
Java 8 中的运行时常量池是指在类或接口中定义的常量的集合,包括编译期间确定的常量和运行期间确定的常量。它是一块位于方法区的内存区域,在类加载时被创建,用于存储类、接口、方法中使用的符号引用,如类名、字段名、方法名等,以及字面量常量(如字符串、数字)。
举个例子,假设有一个类如下所示:
public class Constants {public static final int COUNT = 100;public static final String NAME = "Java";
}
在类加载时,常量池会为该类创建一个常量池表,其中包含了 COUNT 和 NAME 这两个常量的值和引用。当其他类或方法引用 Constants.COUNT 或 Constants.NAME 时,实际上是从常量池中获取对应的值或引用。在运行时,如果需要创建字符串常量或使用字符串常量做参数,也会在常量池中创建相应的字符串对象。
字符串常量池
字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。当 Java 程序中创建字符串对象时,如果该字符串在字符串常量池中已经存在,则返回该字符串的引用;否则将该字符串添加到字符串常量池中,并返回新的引用。
JDK7 开始字符串常量池从方法区移动到堆中。
参考:Java 内存区域详解(重点)、JVM 之 方法区、永久代(PermGen space)、元空间(Metaspace)三者的区别