对象住哪里?——深入剖析 JVM 内存结构与对象分配机制
Java 程序运行时,每一个对象、变量、方法、类信息到底被放在了哪里?
是“堆”还是“栈”?为什么有的对象会被回收,而有的却能长期存在?
一、JVM 运行时内存结构概览
当 Java 程序运行时,JVM 会将可用内存划分为若干功能区域,用于管理不同类型的数据。
这些区域分为 线程私有 与 线程共享 两大类。

说明:
JVM 内存结构”常被用于泛指这部分区域,但从规范角度讲,它是 JVM 的 运行时数据区(Runtime Data Areas)。
1. 程序计数器(PC Register)
每个线程独有,记录当前执行的字节码指令地址。
若执行本地方法(Native),则为空。
占用内存极小,是线程切换后能恢复执行位置的关键。
2. 虚拟机栈(Java Virtual Machine Stack)
每个线程独立,随线程创建而创建。
每次方法调用都会创建一个 栈帧(Stack Frame),其中包括:
局部变量表(Local Variables)
操作数栈(Operand Stack)
动态链接(Dynamic Linking)
方法返回地址(Return Address)
方法调用完成 → 栈帧出栈 → 内存自动释放。
异常情况:
递归过深 → StackOverflowError
栈空间分配失败 → OutOfMemoryError
3. 本地方法栈(Native Method Stack)
专用于执行 JNI(Java Native Interface)方法。
与 JVM 栈类似,但存储的是本地方法调用信息。
4. 堆(Heap)
JVM 最大的内存区域,几乎所有对象实例都在此分配。
是 GC(垃圾回收器) 的主要管理区域。
所有线程共享。
5. 方法区(Method Area)与元空间(Metaspace)
方法区用于存储与类加载相关的元数据信息,包括类的名称、父类、接口、字段与方法定义、运行时常量池、以及 JIT 编译生成的字节码等内容。
在 JDK 8 之前,方法区由 永久代(PermGen) 实现,其中还存放了 静态变量 和 字符串常量池 等数据;
从 JDK 8 开始,永久代被彻底移除,由 元空间(Metaspace) 取而代之。元空间使用 本地内存(Native Memory),不再占用 Java 堆空间。
需要注意的是,自 JDK 7 起,字符串常量池(String Constant Pool) 已被迁移至堆中;
而在 JDK 8 之后,静态变量(static fields) 也被放入堆中进行统一管理。
因此,现代 JVM 中的 方法区/元空间 仅负责存放类的结构性元数据,而不再保存对象实例或静态字段的实际数据。
二、堆与栈的区别
| 对比项 | 虚拟机栈(Stack) | 堆(Heap) |
| 作用 | 保存方法调用过程的局部变量、操作数 | 保存对象实例与数组 |
| 管理者 | 线程私有,自动回收 | 线程共享,由 GC 管理 |
| 生命周期 | 随线程创建与销毁 | 随 JVM 启动与终止 |
| 存储内容 | 基本类型变量、对象引用 | 对象、数组 |
| 是否自动回收 | 是(方法出栈即销毁) | 否(由 GC 控制) |
栈负责执行过程(方法执行、变量存取)
堆负责数据存储(对象与数组的生命周期)。
三、堆内存分区与对象分配机制
在 JVM 规范中,堆(Heap) 被定义为“用于存放对象实例的运行时内存区域”,它是所有线程共享的最大一块内存空间。几乎所有的对象实例与数组都在堆上分配。
不过,这只是规范层面的定义。
真正的堆结构划分,是由 JVM 实现(Implementation) 决定的,而不是规范强制要求的。
目前主流的 HotSpot 虚拟机在实现时,为了提高垃圾回收性能,采用了经典的 “分代收集(Generational Collection)” 策略。
1. 分代收集的设计理念
分代收集理论(Generational Hypothesis)基于两个经验事实:
大多数对象“朝生夕灭” —— 例如方法中临时变量、循环中的局部对象;
少部分对象会存活较长时间 —— 比如缓存对象、全局单例等。
因此,HotSpot 将堆划分为不同的“代”,并为不同生命周期的对象采用不同的垃圾回收策略,从而提升整体效率。
2. HotSpot 的堆划分结构
在 HotSpot JVM 中,堆被逻辑上划分为两大区域:
Heap├── Young Generation(新生代)│ ├── Eden Space│ ├── Survivor Space (From)│ └── Survivor Space (To)└── Old Generation(老年代)
新生代(Young Generation):存放新创建的对象,绝大多数对象会在此被快速回收;
老年代(Old Generation):存放多次 GC 后仍然存活的对象;
新生代内部又细分为:
Eden 区:新对象首先分配的地方;
Survivor 区(From / To):在 Minor GC 后存活的对象会被复制到 Survivor 区中。
3. 对象分配与晋升流程
一个新对象的生命周期(在 HotSpot 中)大致如下:
(1)分配阶段
当 Java 程序执行 new 操作时,JVM 会优先尝试在 Eden 区 为对象分配内存。
(2)Minor GC(年轻代垃圾回收)
当 Eden 区满时,会触发 Minor GC:
回收 Eden 中不再被引用的对象;
存活的对象被移动到空闲的 Survivor 区;
对象的 “年龄计数” +1。
(3)对象晋升(Promotion)
当对象的年龄计数超过一定阈值(默认 15 次),或 Survivor 区空间不足时,
对象会被“晋升”到 老年代(Old Generation)。
(4)Major / Full GC(老年代回收)
当老年代空间不足时,触发 Major GC 或 Full GC,对整个堆进行清理。
对象分配与晋升流程图:
(对象创建) → (Eden 区分配)↓(Eden 满)↓触发 Minor GC → (存活对象复制到 Survivor (From))↓(每次 Minor GC 年龄+1)↓
年龄达到阈值(默认15) → (晋升到老年代)↓(老年代空间不足或元空间溢出)↓触发 Full GC → (标记-清除-整理) → (回收不可达对象)
4. 分代的意义
这种分代策略带来了两个关键优势:
减少整堆扫描次数:大多数 GC 仅作用于年轻代;
针对性算法优化:新生代使用复制算法(Copying),老年代使用标记-压缩算法(Mark-Compact),结合性能与空间效率。
这也是为什么 HotSpot 能在内存管理上兼顾性能与吞吐量的核心原因。
5. 注意:堆分代不是 JVM 规范的一部分
最后要特别强调:
新生代(Young Generation)与老年代(Old Generation)的划分,并不是 JVM 规范要求的内容,而是 HotSpot 虚拟机的具体实现策略。
其他 JVM(如 Azul Zing、OpenJ9)可能采用完全不同的内存模型,例如基于区域(Region-based)或对象表(Object Table)结构。
因此,当我们在文中提到“堆分代结构”时,实际上是指 HotSpot 的实现细节,并非所有 JVM 都必须遵循这一布局。
四、方法区与元空间(Metaspace)
在讨论堆与栈之后,另一个常被提及的区域就是 方法区(Method Area)。它与堆一样,是线程共享的内存区域,但负责的是类级数据的管理,而不是对象实例。
1. 方法区的定义与作用
根据《Java 虚拟机规范(Java Virtual Machine Specification)》:
方法区(Method Area)是 JVM 的一块逻辑内存区域,用于存放类的元数据信息,包括:
类的结构定义(类名、父类、接口信息)
字段与方法信息
运行时常量池(Runtime Constant Pool)
静态变量(Static Fields)
即时编译器(JIT)编译后的代码(Code Cache)
简单来说,方法区是 “类级数据的仓库”。
当类加载器(ClassLoader)将类加载进内存后,JVM 会将与该类相关的元信息都放入方法区中。
值得注意的是:
方法区是一个抽象的逻辑概念,它描述的是 JVM 应该存放“类元信息”的地方;
而具体的实现方式,则由不同虚拟机决定。
在 HotSpot 中,这个逻辑区域曾经由 永久代(PermGen) 实现,后来被 元空间(Metaspace) 取代。
2.从永久代到元空间:演进历史
| 阶段 | 实现方式 | 存储位置 | 关键变化 |
| JDK6 及以前 | 永久代 (PermGen) | 堆外(与堆并列) | 所有类元数据、常量池、静态变量均在永久代 |
| JDK7 | 永久代 (缩减版) | 堆外(与堆并列) | 字符串常量池、静态变量、运行时常量池迁移至堆 |
| JDK8+ | 元空间 (Metaspace) | 本地内存 (Native Memory) | 永久代被移除,类元数据转移到本地内存中 |
“方法区”是逻辑概念;
“永久代 / 元空间”是 HotSpot 的物理实现。
3.为什么会误以为永久代“在堆里”
(1)都由 JVM 管理
永久代虽然位于堆外,但它的内存分配与垃圾回收由 JVM 控制。
例如 Full GC 时,JVM 会同时回收堆与永久代内的无用类。
因此在监控工具或日志中,两者常被放在一起展示,看起来“像在堆里”。
(2) 早期 HotSpot 日志与参数命名模糊
旧版 GC 日志与参数名模糊,比如会显示:
heap memory usage: perm gen
这让很多人误以为永久代属于堆的一部分。
实际上,它与堆并列存在,是 JVM 管理的另一块独立区域。
4. 各版本存储位置变化对比
| 存储内容 | JDK6(永久代) | JDK7(迁移中) | JDK8(元空间) |
| 类元数据(Class Metadata) | 永久代 | 永久代 | 元空间(本地内存) |
| 运行时常量池(Runtime Constant Pool) | 永久代 | 堆中(附属于 Class 对象) | 堆中(仍附属于 Class 对象) |
| 字符串常量池(String Intern Pool) | 永久代 | 堆 | 堆 |
| 静态变量(Static Fields) | 永久代 | 堆 | 堆 |
| 方法字节码(Method Code) | 永久代 | 永久代 | 元空间 |
| 类加载器信息 | 永久代 | 永久代 | 元空间 |
说明:
运行时常量池逻辑上属于“方法区”;
在 HotSpot 的实现中,它作为每个 Class 对象的组成部分,物理上位于堆;
因此可以说“逻辑在方法区、物理在堆”。
5.元空间的作用与存储内容
在 JDK8 之后,HotSpot 使用 元空间(Metaspace) 来取代永久代。
它位于 本地内存(Native Memory) 中,不再受堆大小限制。
| 区域 | 存放内容 |
| 堆(Heap) | 对象实例、字符串常量池、运行时常量池、静态变量 |
| 元空间(Metaspace) | 类的元数据(结构、字段、方法表、字节码、注解、JIT 代码) |
| 本地内存(Native Memory) | 为元空间分配的物理内存,受系统内存限制 |
元空间存放类定义(Class Metadata),
堆存放类实例与常量(Objects, Constants)。
6.什么是“类的元数据”(Class Metadata)
HotSpot 中的类元数据并不只是“类名 + 字段名”,
而是一整套描述类结构与行为的底层信息:
| 类元数据内容 | 说明 |
| 类名(Class Name) | 例如 java/lang/String |
| 父类与接口信息 | 继承与实现关系 |
| 字段表(Field Table) | 字段名、类型、修饰符 |
| 方法表(Method Table) | 签名、字节码、异常表 |
| 常量池引用 | 指向堆中常量池 |
| 类加载器引用 | 关联的 ClassLoader |
| JIT 编译后本地代码指针 | 指向机器码 |
| 注解与内部类信息 | 类级别元注解与 InnerClass 描述 |
换句话说:
元空间保存了 JVM 理解“类”的所有结构性信息。
它告诉 JVM:类是什么、包含哪些方法、如何执行。
7.元空间的内部结构(HotSpot 实现)
HotSpot 的元空间在底层被划分为多个空间段(Space),每种类型有不同作用:
| 区域名 | 作用 |
| Class Space | 存放类的核心元数据(字段、方法、接口信息) |
| Non-Class Space | 存放符号表(Symbol)、类加载器信息等 |
| Chunk Pool | 为不同 ClassLoader 分配的元空间块 |
| Compressed Class Space(可选) | 启用指针压缩(UseCompressedClassPointers)时用于保存类指针映射信息 |
8.实战示例:从代码看对象的“住所”
public class ObjectLocationTest {int value = 42; // 实例字段 → 堆static String name = "JVM"; // 静态字段 → 方法区(JDK8 起为堆中的元空间)public static void main(String[] args) {ObjectLocationTest obj = new ObjectLocationTest(); // 引用变量 obj → 栈int local = 10; // 基本类型局部变量 → 栈String text = "hello"; // 引用变量 text → 栈,指向堆中字符串对象}
}
运行时分布解析:
| 数据项 | 存储位置 | 说明 |
| local | 虚拟机栈(局部变量表) | 属于基本类型变量,直接在栈帧中存储具体数值 |
| obj 引用 | 虚拟机栈 | 引用类型变量本身存储在栈中,指向堆中真实对象 |
| new ObjectLocationTest() 对象 | 堆 | 所有实例对象的实际数据(如 value)都位于堆中 |
| value 实例字段 | 堆 | 属于对象实例的数据部分 |
| name 静态字段 | 方法区 / 元空间(JDK8 之后位于堆中) | 存放在类元数据区域,与实例无关 |
| "hello" 字符串常量 | 字符串常量池(JDK7 起位于堆中) | 常量池中保存的是字符串对象的引用或实例,具体位置依 JDK 版本而异 |
不同 JDK 版本的差异对比
| 版本 | 方法区位置 | 字符串常量池位置 | 说明 |
| JDK6 及以前 | 永久代(PermGen) | 永久代 (PermGen) | 静态变量、类元信息、字符串常量都在永久代 |
| JDK7 | 永久代(PermGen) | 堆中 | 字符串常量池迁移至堆,以缓解永久代空间不足 |
| JDK8 及以后 | 元空间(Metaspace,位于本地内存) | 堆中 | 永久代被彻底移除,类元数据存于本地内存,静态变量在堆中 |
延伸说明:引用与对象的“分家”现象
在 JVM 中,“对象”与“引用”往往分开存储:
引用变量(Reference):在栈帧的局部变量表中,用于指向堆中实际对象。
对象实例(Object Instance):存储在堆中,包含对象头(Mark Word + 类型指针)与实际字段数据。
当方法执行完毕,栈帧被销毁,对象引用随之消失;但对象本身可能仍留在堆中等待 GC 回收。
五、总结:对象究竟住在哪里?
在 JVM 中,对象的“家”在堆中,而引用变量通常存在于栈帧的局部变量表中(若是对象字段或静态变量引用,则位于堆或方法区关联区域)。方法区(或元空间)存放类的元数据与结构定义,字符串常量池在 JDK7 之后迁移到堆中。栈负责方法执行与局部变量存储,堆承载对象与生命周期管理,方法区保存类结构与常量信息。正是这种职责分明的内存划分,使 Java 程序在安全性、稳定性与性能之间实现了平衡。
