JVM内存模型不再难:一个例子看懂堆、栈和方法区
·
JVM内存模型不再难:一个例子看懂堆、栈和方法区
一、先来一段代码
java
public class MemoryDemo {
// 成员变量
private String name = "张三";
private static int count = 0;
public void doSomething() {
int age = 25; // 局部变量
Person p = new Person("李四"); // 引用变量 + 对象
p.setAge(30);
count++;
}
public static void main(String[] args) {
MemoryDemo demo = new MemoryDemo(); // 引用 + 对象
demo.doSomething();
}
}
class Person {
private String name;
private int age;
public Person(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
这段代码很简单,但它完美地覆盖了JVM内存模型的三大核心区域。接下来,我们逐行拆解。
二、JVM内存总览图
┌─────────────────────────────────────────────────┐
│ JVM 内存 │
├──────────────┬──────────────┬────────────────────┤
│ 堆 │ 栈 │ 方法区 │
│ (Heap) │ (Stack) │ (Method Area) │
├──────────────┼──────────────┼────────────────────┤
│ Person对象 │ main方法栈帧 │ MemoryDemo类信息 │
│ - name="李四"│ - args引用 │ - name="张三" │
│ - age=30 │ - demo引用 │ - count=1 │
│ │ │ Person类信息 │
│ MemoryDemo │ doSomething │ - name字段 │
│ - name="张三"│ 栈帧 │ - age字段 │
│ - count=1 │ - age=25 │ - 构造方法字节码 │
│ │ - p引用 │ - setAge字节码 │
│ │ │ 常量池: "张三"等 │
└──────────────┴──────────────┴────────────────────┘
三、三大区域逐一拆解
📦 1. 堆(Heap)——"对象的家"
一句话理解:所有 new 出来的东西,都住在这里。
java
Person p = new Person("李四"); // "李四"这个Person对象住在堆里
MemoryDemo demo = new MemoryDemo(); // demo指向的对象也住在堆里
堆里存了什么?
| 存储内容 | 示例 |
|---|---|
| 对象的实例字段 | Person对象里的 name="李四", age=30 |
| 数组元素 | int[] arr = new int[10],这10个int都在堆里 |
| 成员变量的值 | MemoryDemo对象里的 name="张三", count=1(注意count是static,值存这里,但引用在方法区) |
关键特点:
- ✅ 所有线程共享
- ✅ GC(垃圾回收)的主要战场
- ✅ 运行时动态分配,大小可调(
-Xmx512m)
🧠 记忆口诀:堆 = 仓库,new啥来啥,GC天天扫。
📚 2. 栈(Stack)——"方法的笔记本"
一句话理解:每个方法执行时,会创建一个"栈帧"用来记笔记。
当 main 方法开始执行:
┌───────────── main 栈帧 ─────────────┐
│ 局部变量表: │
│ args ──────→ 字符串数组(堆中) │
│ demo ──────→ MemoryDemo对象(堆中) │
│ 操作数栈: [空] │
│ 帧数据: 方法返回地址等 │
└──────────────────────────────────────┘
当 main 调用 doSomething():
┌───────────── doSomething 栈帧 ────────┐
│ 局部变量表: │
│ age = 25 ← 基本类型,直接存值 │
│ p ─────────────→ Person对象(堆中) │
│ 操作数栈: [空] │
└───────────────────────────────────────┘
↑
┌───────────── main 栈帧 ─────────────┐
│ demo, args ... │ ← main暂停,等doSomething返回
└──────────────────────────────────────┘
栈里存了什么?
| 存储内容 | 示例 | 存的是什么 |
|---|---|---|
| 局部变量(基本类型) | int age = 25 |
直接存 25 这个值 |
| 局部变量(引用类型) | Person p = new Person() |
存的是堆中对象的地址(引用) |
| 方法参数 | doSomething() 无参数 |
同上 |
| 方法返回地址 | 调用完回哪里 | 记录调用关系 |
关键特点:
- ✅ 每个线程独有(线程私有)
- ✅ 方法进栈 → 执行 → 出栈,自动管理
- ✅ 速度极快(仅次于寄存器)
- ❌ 栈太深会
StackOverflowError - ❌ 栈太大(每个帧太大)会
OutOfMemoryError
🧠 记忆口诀:栈 = 笔记本,方法进来记一笔,方法走了笔记撕。
🏛️ 3. 方法区(Method Area)——"类的档案馆"
一句话理解:类的"身份证"信息,全存在这里。
java
// 以下信息都在方法区:
class MemoryDemo {
String name = "张三"; // ✅ 字段名、类型、默认值
static int count = 0; // ✅ static变量的值也在这里
void doSomething(){} // ✅ 方法的字节码指令
}
方法区里存了什么?(以JDK 8+为例,实现叫元空间 Metaspace)
| 存储内容 | 示例 |
|---|---|
| 类的元信息 | 类名、父类、接口、访问修饰符 |
| 字段信息 | 字段名、类型、修饰符 |
| 方法信息 | 方法名、字节码、参数列表 |
| 常量池 | "张三", "李四" 等字符串字面量 |
| static变量 | MemoryDemo.count = 1 这个值 |
| 运行时常量池 | 动态生成的常量(如 String.intern()) |
关键特点:
- ✅ 所有线程共享
- ✅ 存放的是类级别的信息,不是对象级别
- ✅ 永久代(JDK7)→ 元空间(JDK8+,使用本地内存)
- ❌ 加载太多类会
OutOfMemoryError: Metaspace
🧠 记忆口诀:方法区 = 档案馆,类的户口本、身份证、家族谱全在这。
四、用一张图串联整个例子
代码执行: demo.doSomething()
┌──────── 栈 (Stack) ────────┐
│ doSomething 栈帧 │
│ age = 25 [值直接存] │
│ p = 0x7f3a [引用]───────┼──────┐
└─────────────────────────────┘ │
▼
┌──────── 堆 (Heap) ──────────┐ ┌──────────────┐
│ MemoryDemo 对象 │ │ Person 对象 │
│ name → "张三" (堆中) │ │ name → "李四"│
│ count = 1 │ │ age = 30 │
└─────────────────────────────┘ └──────────────┘
┌──────── 方法区 ─────────────┐
│ MemoryDemo 类信息 │
│ - name字段描述 │
│ - count = 1 (static值) │
│ - doSomething() 字节码 │
│ 常量池: "张三" "李四" "30" │
│ Person 类信息 │
│ - name, age 字段描述 │
│ - 构造方法字节码 │
└─────────────────────────────┘
五、常见面试题速答
| 问题 | 答案 |
|---|---|
String s = "hello" 存在哪? |
"hello" 在方法区常量池,s 在栈(存引用) |
String s = new String("hello") 呢? |
"hello" 在常量池,new 出的对象在堆,s 在栈 |
| 静态变量存在哪? | 引用在方法区,值也在方法区(JDK8后static变量值随对象在堆,但类信息仍在方法区,这里要注意区分) |
| 局部变量存在哪? | 基本类型值在栈,引用类型的引用在栈,对象本身在堆 |
| 栈和堆谁大? | 堆通常大得多(-Xmx控制),栈小(-Xss控制,默认1MB) |
| GC只回收堆吗? | 主要回收堆,但方法区的类卸载、常量池也会被回收 |
六、一张表总结
| 特性 | 堆 Heap | 栈 Stack | 方法区 Method Area |
|---|---|---|---|
| 存放内容 | 对象实例、数组 | 局部变量、方法帧 | 类信息、常量、static |
| 线程共享 | ✅ 是 | ❌ 否(线程私有) | ✅ 是 |
| 大小 | 大(可调) | 小(固定) | 中等(可调) |
| 生命周期 | 随对象 | 随方法 | 随类加载器 |
| 错误 | OOM | StackOverflow / OOM | OOM (Metaspace) |
| GC回收 | ✅ 主要目标 | ❌ 自动出栈 | ✅ 类卸载时 |
| 速度 | 较慢 | 最快 | 较慢 |
七、最后一句话总结
堆 存对象,栈 存引用和局部变量,方法区 存类的"身份证"。
记住:new 在堆,定义在栈,类信息在方法区,常量池是方法区的"小抽屉"。
把这三句话背下来,JVM内存模型的面试题,你已经赢了80%。 🎯
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)