这篇文章会用最直白的语言、最清晰的表格和可运行的代码,帮你彻底搞懂 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)局部对象引用
- 引用位置:虚拟机栈的局部变量表
- 实例位置:堆
- 内存图解:
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% 在堆中。
完整分配过程
- 类加载阶段,在元空间中分配静态变量的引用空间
- 类初始化阶段(第一次访问类时),在堆中创建对应的对象实例
- 元空间中的引用指向堆中的实例
内存图解
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 个内存误区
-
❌ 错误:静态变量存在堆中 ✅ 正确:静态变量的引用 存在元空间,实例存在堆中
-
❌ 错误:基本类型数组的元素存在栈中 ✅ 正确:数组本身是对象,所有元素都存在堆中
-
❌ 错误:包装类(Integer、Long)存在栈中 ✅ 正确:包装类是对象,实例存在堆中,栈中只存引用(除了 - 128~127 的缓存)
-
❌ 错误:字符串字面量存在方法区 ✅ 正确:JDK7 及以上,字符串常量池移到了堆中,所有字符串实例都在堆里
-
❌ 错误:对象的方法存在堆中 ✅ 正确:方法的字节码存在元空间,堆中的对象只存成员变量,不存方法
五、总结
- 栈 :存局部变量(基础类型值 + 对象引用),方法执行完自动释放
- 堆 :存所有对象实例和数组,由 GC 负责回收
- 元空间 :存类元信息、静态变量引用、方法字节码,JVM 退出时释放