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%。 🎯

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐