【JVM | 第四篇】—— JVM 内存分配

这篇文章会用最直白的语言、最清晰的表格和可运行的代码,帮你彻底搞懂 JVM 三大核心内存区域的分工,以及所有变量类型的内存分配规则。

如果对JVM内存有不了解的同学可以看我这篇【JVM | 第一篇】------ JVM内存区域详解-CSDN博客

一、核心前提:彻底分清 "引用" 与 "实例"

这是所有 JVM 内存问题的基石,90% 的混淆都源于没分清这两个完全不同的东西。

概念 本质 存储位置 作用 类比
引用(Reference) 一个内存地址(64 位 JVM 开启指针压缩后占 4 字节) 栈、堆、元空间 指向堆中的实际对象 电视机的遥控器
实例(Instance) 实际的对象数据(对象头 + 成员变量值 + 对齐填充) 100% 在堆中(没有任何例外) 存储真正的业务数据 电视机本身

✅可以直接记: 所有通过new关键字创建的东西(对象实例 + 数组),永远只存在堆中。栈、元空间里永远只存引用或基本类型的值,绝对不存实际对象数据。

你拿着遥控器(引用)可以控制电视机(实例),但遥控器不是电视机,电视机也不会跑到你手里(堆里的对象永远不会跑到栈里)。

二、三类变量的完整内存分配详解

2.1 基础类型变量

8 种基本类型(byte/short/int/long/float/double/char/boolean)没有 "引用" 和 "实例" 的概念,直接存储值本身。它的存储位置完全取决于它是 "局部变量" 还是 "成员变量"。

(1)局部基础类型变量(方法里的基础变量)

  • 定义:方法内部定义的基础类型变量
  • 存储位置:虚拟机栈的局部变量表
  • 生命周期:方法执行时创建,方法执行完毕自动释放
java 复制代码
public void test() {
    int a = 10; // 栈中直接存值10,没有引用,没有堆内存
    double b = 95.5; // 栈中直接存值95.5
}

(2)成员基础类型变量(类里的基础变量)

  • 定义:类的非静态成员变量(实例变量)
  • 存储位置:堆中,作为所属对象实例的一部分
  • 生命周期:和所属对象实例一致,对象被 GC 回收时释放
java 复制代码
class User {
    // 当new User()时,age的值会被存到堆中的User对象里
    private int age;
}

2.2 对象实例(所有自定义类、包装类、数组)

创建一个对象分为两个完全独立的步骤,分别使用不同的 JVM 内存区域:

  1. 在堆中分配内存,创建实际的对象实例
  2. 在对应位置创建引用,指向堆中的这个实例

(1)局部对象引用

  • 引用位置:虚拟机栈的局部变量表
  • 实例位置:堆
  • 内存图解:
java 复制代码
栈(局部变量表)        堆
user=0x1234  ----->  0x1234: User对象{name=0x5678, age=25}
                        0x5678: "张三"字符串对象

(2)成员对象引用

  • 引用位置:堆中,作为所属对象实例的一部分
  • 实例位置:堆(另一个独立的对象)
  • 示例:
java 复制代码
class User {
    // name引用存在堆中的User对象里,指向堆中另一个独立的字符串对象
    private String name;
}

(3)数组(特殊的对象)

数组本身是一个对象,100% 存在堆中。无论数组元素是基础类型还是引用类型,全部都存在堆中。

java 复制代码
public void test() {
    // 栈中创建引用arr,堆中创建int数组对象
    int[] arr = new int[5];
    arr[0] = 10; // 10这个值存在堆中的数组对象里,不是栈中
}

2.3 静态变量

这是面试最高频的考点,也是 90% 的人都会记错的部分,请务必记住:

静态变量的引用存在元空间(方法区) ,但它指向的对象实例 仍然100% 在堆中。

完整分配过程

  1. 类加载阶段,在元空间中分配静态变量的引用空间
  2. 类初始化阶段(第一次访问类时),在堆中创建对应的对象实例
  3. 元空间中的引用指向堆中的实例

内存图解

java 复制代码
元空间(方法区)        堆
staticUser=0xdef0  ----->  0xdef0: User对象{name=0x9abc, age=100}
staticAge=20                0x9abc: "静态用户"字符串对象

关键特性

  • 静态变量的生命周期和JVM 进程完全一致,直到 JVM 退出才会释放
  • 所有线程共享同一个静态变量
  • 这就是为什么静态集合会导致严重的内存泄漏:只要 JVM 不退出,静态集合持有的对象永远不会被 GC 回收

三、综合代码逐行拆解

下面这段代码包含了所有常见的变量类型,我逐行标注了每个部分的存储位置,你可以直接复制运行验证。

java 复制代码
public class MemoryAllocationDemo {
    // 1. 静态基础类型:值直接存在元空间
    private static int STATIC_INT = 100;
    // 2. 静态对象引用:引用存在元空间,指向堆中的String实例
    private static String STATIC_STR = "静态字符串";
    // 3. 静态对象引用:引用存在元空间,指向堆中的User实例
    private static User STATIC_USER = new User("静态用户", 100);

    // 4. 成员基础类型:值存在堆中的MemoryAllocationDemo实例里
    private int memberInt = 200;
    // 5. 成员对象引用:引用存在堆中的MemoryAllocationDemo实例里,指向堆中的String实例
    private String memberStr = "成员字符串";

    public static void main(String[] args) {
        // 6. 局部基础类型:值存在main线程栈的局部变量表
        int localInt = 300;
        // 7. 局部对象引用:引用存在main线程栈,指向堆中的User实例
        User localUser = new User("本地用户", 25);
        // 8. 局部数组引用:引用存在main线程栈,指向堆中的int数组实例
        int[] localArr = new int[5];
        localArr[0] = 10; // 10存在堆中的数组实例里

        // 9. 多个引用指向同一个堆实例
        User anotherUser = localUser; // 栈中创建新引用,指向同一个堆对象
        anotherUser.setAge(30); // 修改堆中的对象,localUser也会看到变化
    }

    static class User {
        private String name; // 引用存在堆中的User实例里,指向堆中的String实例
        private int age; // 值存在堆中的User实例里

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

四、5 个内存误区

  1. ❌ 错误:静态变量存在堆中 ✅ 正确:静态变量的引用 存在元空间,实例存在堆中

  2. ❌ 错误:基本类型数组的元素存在栈中 ✅ 正确:数组本身是对象,所有元素都存在堆中

  3. ❌ 错误:包装类(Integer、Long)存在栈中 ✅ 正确:包装类是对象,实例存在堆中,栈中只存引用(除了 - 128~127 的缓存)

  4. ❌ 错误:字符串字面量存在方法区 ✅ 正确:JDK7 及以上,字符串常量池移到了堆中,所有字符串实例都在堆里

  5. ❌ 错误:对象的方法存在堆中 ✅ 正确:方法的字节码存在元空间,堆中的对象只存成员变量,不存方法

五、总结

  • 栈 :存局部变量(基础类型值 + 对象引用),方法执行完自动释放
  • 堆 :存所有对象实例和数组,由 GC 负责回收
  • 元空间 :存类元信息、静态变量引用、方法字节码,JVM 退出时释放
相关推荐
ShineWinsu3 小时前
对于Redis:AOF持久化的解析
linux·数据库·redis·缓存·面试·持久化·aof
wuminyu4 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
考研保研资料分享5 小时前
金融专硕保研经验:清华五道口预推免、北大汇丰夏令营与面试复盘
面试·金融
事圆则缓5 小时前
Kotlin 入门与面试:从空安全、扩展函数到协程
安全·面试·kotlin
ShineWinsu10 小时前
对于Redis:RDB持久化的解析
linux·数据库·redis·缓存·面试·持久化·rdb
丑陋小蚊子15 小时前
项目数据不能公开,就写清交付物
面试·求职招聘·大学生·简历·简历下载
2601_9499506319 小时前
练题簿在线免费刷题 从资料整理到考前自测
面试·职场和发展·pdf·word·刷题
时间的拾荒人21 小时前
Qt 多线程详解:从 QThread 到实战
开发语言·qt·面试
海绵宝宝转agent21 小时前
MySql高频面试八股开源笔记总结
mysql·面试·开源
2601_9620715721 小时前
类变量和全局变量的生命周期有什么区别?
java·开发语言·jvm