堆、栈、常量池都是 JVM 在运行时对内存的逻辑划分。JVM 在启动时会向操作系统申请一大块内存,然后根据自己的规范把它划分成不同的区域,每个区域有不同的用途、生命周期和管理方式。
一、JVM 内存区域总览
text
┌─────────────────────────────────────────────────────┐
│ JVM 内存 │
├─────────────────────────────────────────────────────┤
│ 线程私有(每个线程独立) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 程序计数器 │ │ 虚拟机栈 │ │ 本地方法栈 │ │
│ │ (PC) │ │ (Stack) │ │ (Native) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ 线程共享(所有线程共用) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ 堆 (Heap) │ │ 方法区 (Method │ │
│ │ │ │ Area) │ │
│ │ ┌────────────────┐ │ │ ┌────────────────┐ │ │
│ │ │ 字符串常量池 │ │ │ │ 运行时常量池 │ │ │
│ │ │ (JDK7+ 移到堆) │ │ │ │ (Class常量) │ │ │
│ │ └────────────────┘ │ │ └────────────────┘ │ │
│ └──────────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────┘
二、各区域的定义与用途
1. 虚拟机栈(Stack)
-
归属:线程私有,每个线程创建时都会分配一个栈。
-
存储内容:栈帧(Stack Frame)。每个方法调用时创建一个栈帧,包含:
-
局部变量表:存放基本类型值、对象引用(reference)。
-
操作数栈:执行字节码时的临时工作区。
-
方法出口 、动态链接等。
-
-
生命周期:随线程的创建而创建,随线程的结束而销毁。方法执行完,栈帧出栈。
-
特点 :存取速度快 ,但空间小(默认几百 KB 到 1 MB 左右),超出会抛
StackOverflowError。
2. 堆(Heap)
-
归属:线程共享,整个 JVM 只有一块堆。
-
存储内容 :几乎所有对象实例和数组。
-
生命周期:对象在堆中创建,由 GC(垃圾回收器)负责回收。对象存活时间不确定。
-
特点 :空间大 (可通过
-Xmx设置),但存取速度比栈慢。超出会抛OutOfMemoryError: Java heap space。 -
细分:新生代(Eden + Survivor)和老年代。
3. 方法区(Method Area)
-
归属:线程共享。
-
存储内容 :类的元信息(类结构、字段、方法、构造器)、静态变量、运行时常量池。
-
实现:
-
JDK 7 及之前:用 永久代(PermGen) 实现,在堆中。
-
JDK 8 及之后:用 元空间(Metaspace) 实现,移到了本地内存(不在 JVM 堆中)。
-
-
运行时常量池 :存放编译期生成的各种字面量 和符号引用(类名、方法名、字段名等)。
4. 字符串常量池(String Pool)
-
归属 :从 JDK 7 开始,从方法区移到了堆中(JDK 6 及之前属于永久代)。
-
存储内容 :字符串字面量的引用和对象。使用
String.intern()也会尝试放入此池。 -
目的:复用字符串,节省内存。
-
关键规则:
-
直接用字面量赋值的字符串(如
String s = "abc"),会先检查字符串常量池是否有"abc",有则直接返回引用,没有则创建。 -
用
new String("abc")会在堆中创建一个新对象,同时常量池中也会(或已有)一个"abc"。
-
三、三者的关系
用一个示例来说明它们的关系:
java
public class MemoryDemo {
static int staticVar = 100; // 方法区(静态变量)
String str1 = "hello"; // "hello" 在字符串常量池(堆)
public static void main(String[] args) {
int a = 10; // 栈(局部变量,基本类型)
String str2 = "hello"; // 栈中引用 → 指向字符串常量池
String str3 = new String("hello"); // 栈中引用 → 指向堆中新建对象
Object obj = new Object(); // 栈中引用 → 指向堆中的对象实例
MemoryDemo demo = new MemoryDemo(); // 栈中引用 → 指向堆中的实例
}
}
关系图:
text
栈(线程私有) 堆(线程共享)
┌─────────────────┐ ┌─────────────────────────────┐
│ main 栈帧 │ │ │
│ a = 10 │ │ 字符串常量池: "hello" │
│ str2 ──────────┼──────────────┼──→ "hello" │
│ str3 ──────────┼──────┐ │ │
│ obj ──────────┼───┐ │ │ 堆中对象: │
│ demo ──────────┼─┐ │ └───────┼──→ new String("hello") │
│ │ │ │ │ new Object() │
│ │ │ └──────────┼──→ MemoryDemo 实例 │
│ │ └────────────┼──→ Object 实例 │
└─────────────────┘ └─────────────────────────────┘
方法区(JDK8+ 在本地内存)
┌─────────────────────────────────┐
│ 类的元信息 │
│ 静态变量 staticVar = 100 │
│ 运行时常量池(类级常量) │
└─────────────────────────────────┘
核心关系:
-
栈 中存放的是引用(地址),指向堆或常量池中的实际对象。
-
堆 中存放的是对象实例 ,包括
new出来的字符串对象、普通对象。 -
字符串常量池 (JDK7+ 在堆中)存放字面量字符串,被栈中的引用所指向。
-
方法区 存放类元信息、静态变量、运行时常量池,被所有线程共享。
四、为什么 JVM 要这样划分?
| 区域 | 管理方式 | 目的 |
|---|---|---|
| 栈 | 自动分配/释放(方法调用结束即释放) | 高效管理方法调用和局部变量,速度快 |
| 堆 | GC 管理(对象不再被引用时回收) | 灵活管理对象生命周期,空间大 |
| 方法区 | 类加载时分配,类卸载时回收 | 存放类元信息,全局共享 |
| 常量池 | 编译期确定,运行期可增补 | 复用常量,节省内存,加快访问 |
五、总结
-
是 JVM 对内存的逻辑划分,不是操作系统直接提供的。
-
栈:线程私有,存局部变量和引用,方法结束自动释放,速度快。
-
堆:线程共享,存对象实例,GC 管理,空间大。
-
常量池:JDK7+ 字符串常量池在堆中,运行时常量池在方法区。
-
三者关系:栈中的引用指向堆或常量池中的实际对象;方法区存放类元信息。
-
==比较的是栈中引用的地址是否相同;equals比较的是堆/常量池中对象的内容是否相同。
理解了这些区域的分工,就能明白为什么 str1 == str2 有时是 false,以及 intern() 为什么能让引用指向常量池。