局部变量存于栈帧局部变量表,成员变量嵌入堆中对象内存布局,static变量存于方法区(jdk8+为元空间);三者归属明确、生命周期与作用域严格对应。

局部变量存在栈帧的局部变量表里,成员变量嵌在堆中对象的内存布局里——这是 JVM 管理变量最核心的分工。
局部变量只在栈帧里,不进堆
方法执行时,JVM 为它分配一个栈帧,局部变量(包括形参)就存放在这个栈帧的局部变量表中。基本类型值直接写入表项;引用类型则存一个指向堆中对象的地址。方法结束,栈帧弹出,局部变量表随之销毁——哪怕对象还在堆里,这个“指针”也没了,容易引发 NullPointerException。
- int x = 100 → 值 100 直接存局部变量表第 0 号槽位
- String s = new String("ok") → 引用 s 存在局部变量表,new 出的字符串对象本身在堆
- for 循环里的 i 和循环外新定义的 temp 可能复用同一个槽位,空间不随代码行数增长
成员变量是对象的一部分,必然在堆
只要不是 static,成员变量就和所属对象“长在一起”。JVM 创建对象时,在堆上划出一块连续内存,里面包含对象头、实例数据(即所有非静态字段)、对齐填充——count、list、user 这些字段的值或引用,都按声明顺序排布在这块内存里。
- private int id; → 整数值直接嵌在对象内存块中
- private List
tags = new ArrayList(); → tags 引用存对象体内,ArrayList 实例另在堆中分配(可能不同位置) - this.id 或 obj.name 访问的,本质是通过对象起始地址 + 偏移量读取堆内存
static 变量不归堆也不归栈,它在方法区
static 字段属于类,不是某个对象的属性。JDK 8+ 中它存在元空间(Metaspace),由类加载器管理,生命周期与类一致。它不随对象创建而分配,也不随方法调用而压栈。
- public static final String VERSION = "2.1"; → 编译期确定,常量池引用 + 元空间存储
- private static Map
cache; → 引用存元空间,Map 实例仍落在堆中
逃逸分析不影响语义,只优化分配策略
JVM 可能通过逃逸分析判定某个 new 出的对象不会被其他线程或方法访问,从而尝试栈上分配或标量替换。但这只是底层优化——你写的 this.field 依然逻辑上属于堆中对象,GC 也不会去回收“栈上对象”,因为语义没变。
- 编译器和运行时仍按“成员变量在堆”来解析字节码和生成指令
- 调试器、堆转储(heap dump)看到的仍是标准堆布局,不会出现“某字段在栈里”
- 开发时无需关心是否逃逸,按规范理解即可











