堆 栈 和常量池的关系是啥 是jvm对他们进行的划分吗

堆、栈、常量池都是 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 管理(对象不再被引用时回收) 灵活管理对象生命周期,空间大
方法区 类加载时分配,类卸载时回收 存放类元信息,全局共享
常量池 编译期确定,运行期可增补 复用常量,节省内存,加快访问

五、总结

  1. 是 JVM 对内存的逻辑划分,不是操作系统直接提供的。

  2. :线程私有,存局部变量和引用,方法结束自动释放,速度快。

  3. :线程共享,存对象实例,GC 管理,空间大。

  4. 常量池:JDK7+ 字符串常量池在堆中,运行时常量池在方法区。

  5. 三者关系:栈中的引用指向堆或常量池中的实际对象;方法区存放类元信息。

  6. == 比较的是栈中引用的地址是否相同;equals 比较的是堆/常量池中对象的内容是否相同。

理解了这些区域的分工,就能明白为什么 str1 == str2 有时是 false,以及 intern() 为什么能让引用指向常量池。

相关推荐
周GZ1 小时前
4个提问,deepseek讲解Java中的线程池
java
LuTshoes1 小时前
context 上下文工程
java·人工智能·spring·ai
wuminyu1 小时前
Markword在紧凑对象头上的实现原理剖析
java·linux·c语言·jvm·c++
君顾11 小时前
24小时自助健身房系统开发实战与完整指南
java·开发语言·健身房
鹿角片ljp1 小时前
LeetCode 78:子集|回溯、选与不选、递归和path快照
java·数据结构·算法
金玉满堂@bj2 小时前
多环境部署方案(开发、测试、生产,搭配Tomcat\+WAR包)
java·tomcat·maven
白远山2 小时前
无人自助健身平台搭建:从架构设计到设备联动的完整实战
java·开发语言·架构·需求分析
古法安卓2 小时前
Android-Fork 机制详解
android·java·android studio
曹牧2 小时前
Spring:HttpMessageConverter
java