JVM 运行时内存区域如何划分的?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
基础掌握度:面试官不仅仅是想知道你能不能背出几个区域名称,更是想知道你是否清楚每个区域的作用、存储的内容,以及它们之间的协作关系。纯背八股文和真正理解的区别,面试官一追问就能看出来。
-
线程安全意识:考察你是否区分了线程私有区域和线程共享区域,这直接关系到你对并发问题的理解深度。
-
版本演进认知:JDK 8 对内存区域做了一次大调整(永久代 → 元空间),面试官想看你是否关注过这些变化,而不是停留在老版本的知识上。
核心答案
JVM 运行时内存区域按 线程归属 划分为两大类:
| 分类 | 区域 | 线程归属 | 存储内容 | 异常类型 |
|---|---|---|---|---|
| 线程私有 | 程序计数器 | 线程私有 | 当前执行的字节码指令地址 | 唯一不会 OOM |
| 线程私有 | 虚拟机栈 | 线程私有 | 栈帧(局部变量表、操作数栈等) | StackOverflowError / OOM |
| 线程私有 | 本地方法栈 | 线程私有 | Native 方法的调用信息 | StackOverflowError / OOM |
| 线程共享 | 堆 | 线程共享 | 对象实例、数组 | OutOfMemoryError |
| 线程共享 | 方法区(元空间) | 线程共享 | 类信息、常量、静态变量 | OutOfMemoryError |
还有个常被忽略的 "编外人员"——直接内存,它不属于 JVM 规范定义的运行时数据区,但 NIO 的 DirectByteBuffer 会用到,也可能导致 OOM。
深度解析
一、全局内存布局
先上一张全景图,把 JVM 内存区域的整体结构看清楚:
上图展示了 JVM 运行时内存区域的完整布局,整体分为 线程私有 和 线程共享 两大部分。关键点在于:
-
左侧线程私有区域:每个线程都有自己独立的一份,包括程序计数器、虚拟机栈、本地方法栈。线程之间互不干扰,所以这些区域天然就是线程安全的。
-
右侧线程共享区域:堆和方法区是所有线程共享的,这也是并发问题的 "重灾区"。你在多线程环境下遇到的很多数据竞争问题,根源就在这里。
-
运行时常量池:它逻辑上是方法区的一部分,但在 JDK 7 之后,字符串常量池被移到了堆中,这个版本变化面试官特别爱追问。
二、线程私有区域逐个拆解
1. 程序计数器(Program Counter Register)
这块是最 "小透明" 的区域,但作用很关键。
- 存什么:记录当前线程正在执行的字节码指令地址。如果执行的是 Native 方法,则为空(
Undefined)。 - 为什么需要它:CPU 在多线程之间切换时,需要知道每个线程执行到了哪里,恢复时才能接着往下跑。
- 唯一不会 OOM 的区域:JVM 规范中没有规定任何
OutOfMemoryError情况。
2. 虚拟机栈(VM Stack)
这块是面试的高频考点,栈帧的结构一定要搞清楚。
上图展示了虚拟机栈的内部结构,每个方法调用对应一个栈帧,遵循 "先进后出" 的原则。关键要点:
- 栈帧:每个方法从调用到执行完成,对应一个栈帧的入栈和出栈过程。
- 局部变量表:存放方法参数和局部变量。基本类型直接存值,对象引用存的是指向堆中对象的引用地址。其中
long和double占 2 个 slot,其余占 1 个 slot。 - 操作数栈:方法执行过程中的计算工作区,比如
a + b,先把a和b压栈,再执行加法。 - 动态链接:指向运行时常量池中该栈帧所属方法的引用,支持多态的实现。
- 方法返回地址:方法正常退出或异常退出后,返回到调用者的位置。
当递归调用太深时,栈帧不断入栈但不出栈,就会抛出 StackOverflowError。
3. 本地方法栈
和虚拟机栈类似,只不过它服务的是 Native 方法(比如 System.currentTimeMillis() 底层调用的就是 Native 方法)。HotSpot 虚拟机把虚拟机栈和本地方法栈合二为一了。
三、线程共享区域
1. 堆(Heap)
堆是 JVM 中最大的一块内存,几乎所有对象实例和数组都在这里分配。
- 新生代:新对象先在 Eden 区分配。Eden 区满时触发 Minor GC,存活对象复制到 Survivor 区,年龄达到阈值(默认 15)晋升到老年代。默认比例 Eden : S0 : S1 = 8 : 1 : 1。
- 老年代:存放长期存活的对象和大对象(直接进入老年代的场景)。老年代满时触发 Major GC / Full GC。
这块有个经典考点:几乎所有对象都在堆上分配。为啥说 "几乎"?因为 JDK 7 之后引入了逃逸分析和标量替换,如果一个对象没有逃逸出方法,JVM 可能会直接在栈上分配,避免堆内存的分配和 GC 压力。
2. 方法区(Method Area / 元空间)
方法区在 JDK 7 及以前叫 "永久代"(PermGen),JDK 8 开始改名为 "元空间"(Metaspace),并且从 JVM 堆内存移到了本地内存。
| 对比项 | JDK 7 永久代 | JDK 8+ 元空间 |
|---|---|---|
| 存储位置 | JVM 堆内存 | 本地内存(直接内存) |
| 大小限制 | 受 -XX:MaxPermSize 限制 |
受物理内存限制(也可配置上限) |
| 存储内容 | 类信息、常量池、静态变量 | 类信息、常量池(字符串常量池已移至堆) |
| 常见问题 | 容易 OOM: PermGen space |
较少出现,但动态生成大量类时仍可能 OOM |
| GC 触发 | Full GC 时回收 | 达到 MaxMetaspaceSize 时触发 Full GC |
为什么要干掉永久代?因为永久代的大小很难预估,太大浪费内存,太小动不动就 OOM,尤其是用 CGLIB、动态代理大量生成类的框架(比如 Spring),永久代经常爆。元空间直接用本地内存,空间大了不少,这个改动确实优雅。
3. 运行时常量池
它是方法区的一部分,存放编译期生成的各种字面量和符号引用。JDK 7 之后,字符串常量池被移到了堆中。
// 经典面试代码:体会常量池的位置变化
String s1 = new String("hello"); // 堆上创建对象
String s2 = "hello"; // 字符串常量池中查找或创建
String s3 = "hello";
System.out.println(s2 == s3); // true,指向常量池同一个引用
System.out.println(s1 == s2); // false,s1 在堆上,s2 在常量池
这段代码的要点在于:new String() 会在堆上创建一个新对象,而字面量赋值 "hello" 会先去字符串常量池查找,有就直接引用,没有就创建。这也是为什么 s2 == s3 为 true,而 s1 == s2 为 false。
面试高频追问
-
追问:JDK 8 为什么用元空间替换永久代?
三个核心原因:永久代大小难以预估容易 OOM;字符串常量池已经在 JDK 7 移到了堆中,永久代存在意义减弱;合并 HotSpot 和 JRockit 虚拟机时,JRockit 没有永久代的概念。
-
追问:什么情况下栈会溢出?堆会溢出?
栈溢出通常是递归调用过深(
StackOverflowError)或者线程数过多导致栈总内存不够(OOM)。堆溢出通常是内存泄漏或者创建了大量无法回收的对象(OOM: Java heap space)。 -
追问:直接内存是什么?和堆内存有什么区别?
直接内存(Direct Memory)不受 JVM 堆大小限制,通过
NIO的DirectByteBuffer分配,避免了 Java 堆和 Native 堆之间的数据拷贝,但分配和回收成本更高,也可能导致 OOM。
常见面试变体
- "说说 JVM 的内存模型"(注意区分内存模型 JMM 和内存区域,这是两个不同的概念)
- "JDK 7 和 JDK 8 的 JVM 内存区域有什么区别?"
- "哪些区域会发生 OOM?哪些不会?"
- "方法区和永久代是什么关系?"
记忆口诀
线程私有:计数器、虚拟机栈、本地方法栈("计栈本"——记下本子)
线程共享:堆、方法区("对方"——堆和方法)
OOM 归属:计数器不会 OOM,栈会溢出(StackOverflow),堆和方法区会 OOM
总结
JVM 运行时内存区域分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)两大类。面试时记住 "计栈本,对方堆" 就能快速回忆起所有区域,再结合每个区域存什么、会不会 OOM、JDK 7 到 8 的变化(永久代 → 元空间)来展开,这道题基本稳了。
