3. JVM 运行时内存区域
本篇我们将深入探讨 JVM 运行时数据区(Runtime Data Area),也就是大家常说的 JVM 内存结构。
对于后端开发(比如写 Spring Boot)来说,这是整个 JVM 中最核心、面试考得最多、排查线上崩溃最需要的部分。
3. 核心分类:谁的隐私?谁的公共财产?
在拆解具体区域之前,我们需要先引入一个核心概念:线程(Thread)。
什么是线程?
你可以把“线程”理解为后端程序里一个独立的“执行任务的工人”。在 Spring Boot 中,每当有一个前端请求访问你的接口,服务器就会派出一个专门的线程去处理这个请求。
JVM 的内存区域根据“这个区域是属于某个具体线程私有的,还是所有线程都能一起访问的”,分成了两大类:
- 线程私有区:每个工人(线程)自己专属的独立小账本,各看各的,互不干扰。随着线程创建而诞生,随着线程结束而销毁。
- 线程共享区:工厂里所有工人(线程)都能一起访问的公共大仓库。程序一启动就创建,程序关闭才销毁。
4. 拆解五大内存区域
1. 堆(Heap)—— 【线程共享】
- 存放什么:存放所有创建的对象实例。
- 后端对应:你在代码里所有通过
new关键字创建出来的东西(比如User user = new User();),以及 Spring 容器帮你管理的各种 Bean(如UserServiceImpl),它们的实体数据全都整整齐齐地躺在堆内存里。 - 特点:这是 JVM 中最大的一块内存区域。因为所有人都能往里塞对象,也是最容易发生内存溢出(OOM)的地方。同时,它也是下一章要讲的“垃圾回收器(GC)”最主要的工作战场。
2. 虚拟机栈(JVM Stack)—— 【线程私有】
- 存放什么:存放正在执行的方法信息、局部变量和对象引用。
- 工作机制:每当一个线程调用一个方法时,JVM 就会在这个栈里为该方法开辟一块专有的空间,叫做栈帧(Stack Frame)。
- 方法里定义的局部变量(如
int age = 18;)直接存放在栈帧里。 - 方法里定义的对象变量(如
User user),这个变量本身(也就是指向堆内存的“地址指针”)存放在栈里,而真正的User对象实体依然在堆里。
- 方法里定义的局部变量(如
- 特点:后进先出(就像弹夹压子弹)。方法 A 调用方法 B,方法 B 的栈帧就会压在 A 的上面;方法 B 执行完退出了,B 的栈帧就被弹走销毁。
3. 元空间(Metaspace)—— 【线程共享】
- 存放什么:存放类的信息(Class MetaData)、常量池、静态变量等。
- 后端对应:当你写好一个类,JVM 把
.class文件加载进来后,这个类叫什么名字、有哪些方法、有哪些成员变量、带有@RestController哪些注解……这些关于类本身的设计图纸信息,都存在元空间。 - 特点:在早期的 Java 版本中它叫“永久代(PermGen)”,现在改名叫“元空间”,并且它直接使用的是计算机的物理内存,不再受制于 JVM 初始分配的固定内存大小。
4. 程序计数器(Program Counter Register)—— 【线程私有】
- 存放什么:存放当前线程正在执行的下一条字节码指令的地址。
- 工作机制:CPU 的运行速度极快,会在多个线程之间来回切换(一会儿处理 A 请求,一会儿处理 B 请求)。当线程 A 被 CPU 暂停切换走,过会儿再切回来时,它怎么知道自己刚才执行到代码的哪一行了?全靠程序计数器指路。
- 特点:它是一块极小的内存,也是 JVM 规范中唯一一个绝对不会发生内存溢出(OOM)的区域。
5. 本地方法栈(Native Method Stack)—— 【线程私有】
- 存放什么:和虚拟机栈极其相似,区别在于:虚拟机栈是为 JVM 执行 Java 方法服务的;而本地方法栈是为 JVM 调用 Native 方法(底层 C/C++ 方法)服务的。
5. 用一段真实的后端代码,看懂内存分配
光看概念很抽象,我们直接来看一段普通的 Java 核心代码,看看它们在内存里到底是怎么放的:
public class OrderService {
// 静态变量,存放在【元空间】
public static String CONFIG_NAME = "OrderConfig";
public void createOrder() {
// age 是基本类型的局部变量,它的值 "20" 直接存放在【虚拟机栈】的栈帧里
int age = 20;
// order 是一个局部变量(对象引用),存放在【虚拟机栈】中
// new Order() 创建出来的真实对象实体,存放在【堆】中
Order order = new Order();
}
}
当这段代码运行起来时:
OrderService的类结构、注解信息和静态变量CONFIG_NAME被安置在元空间。- 线程进来执行
createOrder()方法,在虚拟机栈里生成一个栈帧。 - 栈帧里塞进了
age = 20。 - 堆内存里划分了一块空间把
new Order()的实例放进去。 - 栈帧里产生一个叫做
order的指针,远远地指向堆里那个刚刚创建的Order实例。
至此,JVM 内部的内存世界已经为你敞开了。在写后端代码时,我们要时刻意识到,我们创建的每一个对象、调用的每一个方法,都在上面这些区域里悄悄挪动着空间。
理解了这五大区域的职能和划分之后,我们不禁会思考一个问题:这些留在堆里的对象,不再使用后是如何被清理掉的? 这就引出了我们下一篇的核心话题:垃圾回收机制。