当前位置: 首页 > news >正文

对象住哪里?——深入剖析 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 程序在安全性、稳定性与性能之间实现了平衡。

http://www.dtcms.com/a/582814.html

相关文章:

  • 站长工具高清有吗网推获客平台
  • 网站做流量的论坛贴吧广东广东网站建设工作
  • 中国机械加工网下载番禺seo
  • 如何让美颜SDK更智能?AI赋能下的个性化美颜功能设计详解
  • 软件开发模型——瀑布式模型:软件开发的经典范式及其现代实践
  • 网站集约化建设讲话上海知名的网站建设
  • WordPress建站要花钱七牛图片处理 wordpress
  • 认识linux -- 调试器 - gdb/cgdb的使用
  • 神经流形:大脑功能几何基础的革命性视角
  • 杭州做网站外包公司网站建设实训报告总结
  • 高新区建设局网站网站建设与开发试卷
  • 测试跟踪步骤描述用例交互优化,MeterSphere开源持续测试工具v2.10.26 LTS版本发布
  • CSMA(aloha)
  • 衡水做网站优化黄页网址大全免费
  • 苍穹外卖 —— Spring Cache和购物车功能开发
  • 建设网站毕业设计河南城乡建设厅网站证书查询
  • 留言网站模板沈阳百度seo代理
  • top域名的网站打不开平台软件
  • 新开传奇手游网站大全jn建站系统官网
  • 【AI安全】提示词注入
  • 两个人做类似的梦 网站咨询类网站建设方案书
  • 企业年报详情查询API——在线查询企业年报信息的可靠工具
  • StarGAN标签是怎么传给神经网络的?作为数据中的一个或几个维度吗?
  • 重庆忠县网站建设公司哪家专业移动互联网开发考研方向
  • 基于图扑自研 HT 搭建的园区元宇宙可视化管理平台
  • wordpress dux 下载一键优化清理神器
  • 北京网站排名优化公司美工做任务网站
  • 广州企业网站建设公司哪家好东莞网站推广多少钱
  • 【Go】--log模块的使用
  • 交互式参数控制面板:Panel与Bokeh Server深度解析