Appearance
Java 内存划分详解
很多人第一次学 Java 内存,会把它背成几块区域:
- 堆
- 栈
- 方法区
- 程序计数器
这样记不能说错,但还是太薄了。真正到了需要继续展开的时候,更可能继续追问:
- 这些区域分别存什么
- 哪些是线程私有,哪些是线程共享
- 堆为什么还要分新生代和老年代
StackOverflowError和OutOfMemoryError分别通常出现在什么地方- 方法区、永久代、元空间到底是什么关系
这篇文章就把 Java 的内存划分从“能背名词”讲到“能解释原理”。
1. Java 内存划分到底在说什么
“Java 内存划分”通常就是指 JVM 运行时数据区的划分。
也就是 Java 程序跑起来之后,JVM 会把运行时需要的内存按职责拆成不同区域,分别负责:
- 记录线程执行位置
- 管理方法调用过程
- 存放对象实例
- 存放类元数据和常量
- 支撑本地方法调用
这样做的好处是:
- 职责更清晰
- 管理方式不同
- 回收策略也可以不同
2. 先从大图上看:线程私有 vs 线程共享
最常见的一种记忆方式,是先按“线程维度”来分。
2.1 线程私有
每个线程都会有自己独立的一份:
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
这些区域不会被其他线程直接共享,所以天然更贴近“线程执行过程”。
2.2 线程共享
多个线程共同使用:
- 堆
- 方法区
它们更贴近“整个应用运行时的公共数据”。
如果只想先记一个框架,可以直接记成:程序计数器、虚拟机栈、本地方法栈是线程私有;堆和方法区是线程共享。
3. 程序计数器
3.1 它是什么
程序计数器可以理解成 当前线程下一条将要执行的字节码指令位置记录器。
因为 Java 程序在 JVM 里执行时,本质上是在线程之间切换着跑的。线程一旦被挂起,再恢复时必须知道自己之前执行到了哪里。
这时候程序计数器就派上用场了。
3.2 它负责什么
它主要负责:
- 记录当前线程执行位置
- 在线程切换后恢复正确执行点
- 配合字节码解释执行流程
3.3 它的特点
- 线程私有
- 生命周期跟线程一致
- 占用内存通常很小
- JVM 规范里,它是少数不会规定出现
OutOfMemoryError的区域
4. Java 虚拟机栈
4.1 它是什么
虚拟机栈是 Java 方法执行时的内存模型。
更具体地说:每次方法调用,都会对应一个栈帧入栈;方法执行结束,对应栈帧出栈。
所以它最适合从“方法调用过程”来理解。
4.2 栈帧里有什么
一个栈帧里通常至少包括:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
你可以抓住最重要的两个:
- 局部变量表:存方法参数和局部变量
- 操作数栈:执行字节码计算时临时使用
4.3 一个简单例子
java
public int add(int a, int b) {
int c = a + b;
return c;
}方法执行时:
a、b、c这类局部变量会进入当前栈帧- 计算过程会用到操作数栈
- 方法返回时,当前栈帧出栈
4.4 常见异常
最典型的是:StackOverflowError
例如无限递归:
java
public void test() {
test();
}因为方法不断入栈,栈深度持续增加,最终就会栈溢出。
在某些极端情况下,栈扩展失败也可能导致 OutOfMemoryError,不过平时更常见的情况还是:
递归过深更容易导致 StackOverflowError。
5. 本地方法栈
5.1 它是什么
本地方法栈和虚拟机栈很像,只不过它服务的是:native 方法。
也就是那些不是用 Java 本身实现,而是通过 C/C++ 等本地代码实现的方法。
5.2 它的作用
当 Java 程序调用本地方法时,本地方法栈会为这些调用过程提供支持。
你可以简单记成:
- Java 方法主要走虚拟机栈
- native 方法主要走本地方法栈
5.3 为什么它存在
因为 JVM 并不是一个完全封闭的世界。它需要:
- 调用操作系统能力
- 调用本地库
- 支持底层高性能组件
这就是 JNI 等机制存在的背景。
6. 堆
6.1 它是什么
堆是 Java 内存里最核心的一块区域,也是 GC 重点工作的地方。
最常见的理解是:对象实例大多分配在堆上。
比如:
java
User user = new User();这里 new User() 创建出来的对象,通常就在堆里。
6.2 为什么堆重要
因为 Java 里的绝大多数对象生命周期管理,都发生在堆中。
堆负责的主要是:
- 对象实例存储
- 数组对象存储
- 垃圾回收的主要空间
6.3 堆为什么还要继续细分
因为对象并不是都活得一样久。
现实里经常是:
- 大量对象很快就死掉
- 少数对象会长期存活
如果所有对象都混在一起回收,成本会很高。
所以 HotSpot 通常会把堆再继续划分:
- 新生代
- 老年代
7. 新生代和老年代
7.1 新生代
新生代主要存放“新创建出来、生命周期偏短”的对象。
它通常还会再分成:
- Eden 区
- Survivor From
- Survivor To
大多数对象创建时先进入 Eden。
经历一次垃圾回收后:
- 活下来的对象会被复制到 Survivor 区
- 在 Survivor 中多次存活后,可能晋升到老年代
7.2 老年代
老年代主要存放:
- 生命周期较长的对象
- 大对象
- 从新生代晋升上来的对象
7.3 为什么要这样分代
因为不同生命周期的对象,适合不同的回收策略。
这就是分代回收的核心思想:让短命对象和长命对象分开管理,提高回收效率。
8. 方法区
8.1 它是什么
方法区是 JVM 规范里定义的一块逻辑区域,用来存放类级别的信息。
它主要存:
- 类信息
- 运行时常量池
- 静态变量
- 即时编译后的代码缓存等
你可以把它和堆区分开:
- 堆更偏对象实例
- 方法区更偏类元数据
8.2 永久代和方法区是什么关系
这是很高频的易错点。
正确理解是:
- 方法区是 JVM 规范里的概念
- 永久代是 HotSpot 早期对方法区的一种实现方式
也就是说:永久代不是方法区本身,而是方法区的一种实现。
8.3 元空间又是什么
从 Java 8 开始,HotSpot 移除了永久代,改用:Metaspace,元空间
来实现方法区。
所以现在更准确的说法是:
- 规范层面叫方法区
- HotSpot 在 Java 8 以后常说元空间
8.4 常见异常
如果类元数据不断增长,可能出现:OutOfMemoryError: Metaspace
常见诱因:
- 动态生成大量类
- 类加载器泄漏
- 热部署场景回收不及时
9. 运行时常量池
运行时常量池可以理解成方法区中的一部分。
它存放的是:
- 编译期生成的字面量
- 符号引用
- 运行期也可能加入的新常量
比如字符串相关问题里,常量池就是很高频的考点。
要注意:
- 运行时常量池属于方法区的逻辑范畴
- 字符串常量池在 HotSpot 的具体实现上,又有版本差异
所以答题时最好区分:
- 规范怎么定义
- HotSpot 怎么实现
10. 栈和堆到底怎么区分
这是最容易被问成场景题的一块。
10.1 先说一个不容易错的版本
你可以这样理解:
- 方法调用过程主要依赖栈
- 对象实例大多在堆上
- 局部变量通常跟着栈帧走
10.2 为什么不能说得太绝对
因为现代 JVM 会做很多优化,比如:
- 标量替换
- 栈上分配
- 逃逸分析
所以如果把这个问题压缩成一段完整说明得很深,就不要把它说死成:所有对象都一定在堆上。
更稳妥的说法是:通常情况下对象实例主要分配在堆上,方法调用和局部变量主要在栈相关结构里管理,但具体实现会受到 JVM 优化策略影响。
11. 常见异常分别对应哪里
这部分很适合快速拉开理解层次。
11.1 StackOverflowError
常见来源:
- 无限递归
- 递归层次过深
- 单次调用栈帧过大
核心区域:虚拟机栈
11.2 OutOfMemoryError: Java heap space
常见来源:
- 对象创建太多
- 内存泄漏
- 大对象堆积
核心区域:堆
11.3 OutOfMemoryError: Metaspace
常见来源:
- 动态代理生成过多类
- 类加载器泄漏
- 热部署反复加载类
核心区域:方法区 / 元空间
12. 一张图把它们串起来
可以把 JVM 内存结构压缩成下面这个脑图:
text
JVM 运行时数据区
├── 线程私有
│ ├── 程序计数器
│ ├── Java 虚拟机栈
│ └── 本地方法栈
└── 线程共享
├── 堆
│ ├── 新生代
│ │ ├── Eden
│ │ ├── Survivor From
│ │ └── Survivor To
│ └── 老年代
└── 方法区
└── 运行时常量池13. 用一段话把“Java 内存是怎么划分的”讲清楚
顺着下面这个顺序往下讲会更顺:
Java 的内存划分通常指 JVM 运行时数据区,主要包括程序计数器、虚拟机栈、本地方法栈、堆和方法区。程序计数器、虚拟机栈、本地方法栈是线程私有的;堆和方法区是线程共享的。程序计数器负责记录线程当前执行位置,虚拟机栈负责方法调用时的栈帧管理,本地方法栈服务 native 方法,堆主要存对象实例,也是 GC 的核心区域,而方法区主要存类信息、静态变量、常量等。堆里通常还会分新生代和老年代;方法区在 HotSpot 早期通过永久代实现,Java 8 以后更多对应元空间。
14. 几个容易混的点
14.1 堆和栈的区别是什么
把这 3 个点分开:
- 栈更贴近方法调用过程,线程私有
- 堆更贴近对象实例存储,线程共享
- 栈的分配和回收通常更直接,堆主要依赖 GC
14.2 为什么要分新生代和老年代
根子就在这里:因为不同对象生命周期差异很大。大多数对象朝生夕死,少数对象长期存活,所以把它们分开管理,可以用更合适的回收策略提升 GC 效率。
14.3 方法区和永久代是一个东西吗
实际写项目时,更多是这样:不能完全画等号。方法区是 JVM 规范定义的一块逻辑区域,永久代只是 HotSpot 早期对方法区的一种实现;Java 8 以后永久代被移除,改成元空间。
15. 总结
把 Java 内存划分压缩到最核心,其实就是三层:
- 线程执行相关:程序计数器、虚拟机栈、本地方法栈
- 对象存储与回收相关:堆
- 类元数据相关:方法区
如果你能把“线程私有 vs 线程共享、堆分代、方法区和元空间的关系、常见异常对应区域”这几条线讲顺,这一块基本就已经够应对大多数继续追问了。