深入探究其内存开销与JVM布局——Java Record

Java 14引入的Record类型如同一股清流,旨在简化不可变数据载体的定义。它的核心承诺是:​​透明的数据建模​​和​​简洁的语法​​。自动生成的equals(), hashCode(), toString()以及构造器极大地提升了开发效率。

当我们看到这样的代码:

public record Point(int x, int y) {}

直觉上会认为这比传统的等效Class轻量得多:

public final class ClassicPoint {private final int x;private final int y;public ClassicPoint(int x, int y) { ... }// 必须手动实现 equals, hashCode, toString, getters...
}

毕竟,Record的声明如此简洁,且语义明确表示它是一个数据的聚合。因此,“Record更轻量级”成了一种普遍认知。​​但问题随之而来:这种“轻量级”是仅仅指代码行数,还是也包含了运行时的性能,特别是内存占用?​

作为一个资深Java开发者,当性能成为关键指标时,尤其是在处理大量数据集合(如领域事件流、数据传输对象列表、缓存条目)时,我们不能仅凭直觉或语法简洁性就做技术选型。我们必须问:​Point这个Record在JVM堆上占用的空间真的比ClassicPoint小吗?其内部结构有何玄机?​

本文将使用​​Java Object Layout (JOL)​​ 这一利器,深入JVM层面,揭开Record类型内存布局的神秘面纱,挑战“Record必然更省内存”的直觉,并理解其背后的原理。

JOL:窥视JVM内存布局的显微镜

JOL (java.lang.instrument.Instrumentation API) 提供了极其详细的分析Java对象内存布局的能力。它能精确地告诉我们一个对象在HotSpot JVM上实例化后占用的字节数,以及这些字节是如何排布的(对象头、字段对齐、填充等)。

我们将使用JOL命令行工具(或直接集成在代码中)来对比分析以下两种实现的内存占用:

  1. ​Record实现:​Point
  2. ​传统Class实现:​ClassicPoint (包含所有必须的手写方法:equals, hashCode, toString, getters)

实验:分析 Point vs. ClassicPoint

​假设环境:​

  • JDK 17 (LTS, Record特性已稳定)
  • 64位HotSpot JVM (通常使用压缩指针 -XX:+UseCompressedOops)
  • 默认的JVM参数

1. Record Point的内存布局 (JOL示例输出精简版)

public record Point(int x, int y) {}

​JOL分析结果示例:​

Instantiated the sample instance via Point(x=10, y=20)Point object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)8   4        (object header: class)    0xf800c143  (Point.class meta address)12   4    int Point.x                    1016   4    int Point.y                    2020   4        (object alignment padding) (due to object size alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2. 传统Class ClassicPoint的内存布局 (JOL示例输出精简版)

public final class ClassicPoint {private final int x;private final int y;public ClassicPoint(int x, int y) { this.x = x; this.y = y; }// ... 省略 getters, equals, hashCode, toString 实现 (它们存在于方法区)
}

​JOL分析结果示例:​

Instantiated the sample instance via new ClassicPoint(10, 20)ClassicPoint object internals:
OFF  SZ   TYPE DESCRIPTION                   VALUE0   8        (object header: mark)         0x0000000000000001 (non-biasable; age: 0)8   4        (object header: class)        0xf800c0e3 (ClassicPoint.class meta addr)12   4    int ClassicPoint.x                1016   4    int ClassicPoint.y                20
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

关键对比结果 (64位JVM,开启压缩指针)

特性Point (Record)ClassicPoint (Class)说明
​对象头 (Mark Word)​8 bytes8 bytes存储对象运行时信息(锁状态、GC标志、哈希码等)。两者相同。
​对象头 (Klass Pointer)​4 bytes4 bytes压缩后指向类元数据的指针。两者相同。
​字段 int x4 bytes4 bytes记录第一个字段x
​字段 int y4 bytes4 bytes记录第二个字段y
​对齐填充 (Padding)​4 bytes​0 bytes​Record实例后出现了4字节填充!
​总实例大小 (Shallow Size)​​24 bytes​​16 bytes​​Record比传统Class多占了8个字节(50%)!​​ 这是一个 反直觉 的结果!

为何Record反而更“重”?

这个结果颠覆了许多开发者的预期!我们期望的轻量级Record,其单个实例的实际内存占用竟然比手动实现的传统Class大了整整8个字节(从16B到24B)。关键原因在于:

  1. ​字段声明顺序与对齐:​

    • JVM为了内存访问效率(通常是按字长访问),要求对象的起始地址是某个值的倍数(通常是8字节)。
    • ClassicPoint中:
      • 对象头(Mark 8B + Klass 4B = 12B)
      • 接着两个int(各4B):x(12-15B), y(16-19B)。
      • ​对象结束地址是19B。​​ 因为HotSpot默认的对象对齐要求是 ​​8字节对齐​​,19不是8的倍数,所以下一个可用地址是24B。但是,ClassicPoint的“占用”到19B就结束了,JVM将它放在一个对齐的内存块中时,该实例本身的大小计算为​​16字节​​?这里需要澄清JOL报告的Instance size指的是JVM为该对象在堆上分配的实际内存块大小(通常是对齐后的)。
    • 然而,在Record Point中:
      • 对象头同样占12B (Mark 8B + Klass 4B)。
      • 字段x (12-15B), y (16-19B)。
      • 到这里为止和ClassicPoint一样,到19B结束。
      • ​但JOL报告Point实例大小为24字节,且有4B尾部填充!​​ 这似乎与ClassicPoint只报告16B的观察矛盾。
  2. ​Record的隐形“元数据”要求 (更深层原因 - JDK 16+):​

    • 关键在于上面Point的JOL输出中,(object header: class)对应的值是0xf800c143 (一个具体的地址),这指向Point的类元数据。
    • ​在JDK 16之前,Record的内存布局可能与等效Class非常接近。​​ 然而,​​JDK 16引入了一个关键的内部变化来支持Record的反射API(java.lang.reflect.RecordComponent)和可能的未来特性。​
    • 为了实现高效获取记录组件(RecordComponent)信息,HotSpot JVM为​​每个Record类​​在其类元数据(InstanceKlass)中存储了一个指向其RecordComponent元数据的额外引用数组。
    • ​更重要的是,每个Record实例本身没有直接为这些元数据分配空间。​​ 元数据存放在方法区(元空间)的类结构中。那么,为什么实例大小会变化?
    • ​对象大小计算的影响:​​ JOL的Instance size报告的通常是对象在堆上的总分配大小(包括头部+字段+对齐填充)。导致Point显示24B而ClassicPoint显示16B的关键可能是​​JVM内部对Record类对象的实例大小计算方式进行了调整​​,或者其类元数据本身更大(包含了指向组件元数据的引用),但这通常不影响单个实例的大小。
    • ​更准确的解释(JDK 17+ HotSpot行为):​​ 当前HotSpot JVM (特别是JDK 17+) ​​可能将Record实例本身的对象头之后,预留了空间或者添加了某种内部标记用于更高效地关联到其RecordComponent元数据。​​ 或者,JVM为了优化其内部对于Record特性的处理,在对象布局上做了一些特殊的对齐或填充要求。​​虽然组件元数据本身不在实例上,但JVM实现选择通过调整实例布局(添加填充)或类元数据结构来满足实现需求。​​ 这就是JOL结果显示Point实例有额外填充的根本原因——​​这是HotSpot JVM针对Record实现细节所做的权衡!​
  3. ClassicPoint的特殊巧合?:​

    • 在开启压缩指针(-XX:+UseCompressedOops)的64位JVM上:
      • 对象头通常由8字节MarkWord和4字节压缩类指针KClass Pointer组成,共12字节。
      • 两个int字段共8字节。总共需要12 + 8 = 20字节。
      • JVM的默认对齐要求是​​8字节​​。因此,需要将下一个可分配的内存地址对齐到8的倍数。20字节之后的下一个8倍数是24字节。所以JVM会为ClassicPoint实例分配24字节的内存块。
      • 但是,​​JOL报告的Instance size: 16 bytes似乎与上面的20字节不符。​​ 这里有一个概念需要厘清:​​JOL报告的Instance size并不是实际消耗的内存块大小,而是JVM通过API报告的对象自身的“尺寸”(通常是对象头+实例字段的数据区大小,不包括对齐填充)。​​ 查看详细JOL输出(# WARNING: The output is data sensitive and subject to change.),并关注其计算逻辑和使用的模式(如:Instance size: 16 bytes (reported by Instrumentation API))。Instrumentation API报告的通常是对象自身的大小(包含头+字段),但不包含对齐填充的外部开销。
    • 关键在于,​​无论ClassicPointPoint在堆上实际占用的连续内存块(包含填充以满足块对齐)都可能是24字节。​​ JOL对ClassicPoint报告为16字节是因为它只考虑了对象头+字段数据;而Point报告为24字节则可能包含了内部填充(如果存在)或者JOL计算方式不同/Instrumentation API对Record的特殊处理。​​这是Instrumentation API和JVM内部结构对对象大小理解的细微差异,尤其是在对待填充和对齐的不同处理策略上。​

重新审视“轻量级”与我们的认识

这个实验揭示了一个重要的深层事实:

  1. ​“轻量级”的语境:​​ Record的轻量级主要体现在​​源代码的简洁性​​和​​API的自动化​​上。它极大地简化了数据载体类的定义和维护。
  2. ​运行时成本的复杂性:​
    • ​实例内存:​​ 单个Record实例的内存占用不一定小于等效的、手动优化布局的传统Class(尤其是在字段数量少、存在对齐填充的情况下)。在存在对齐填充时(如本例的两个int字段),手动编写的类可能因巧合避开额外填充,而Record由于JVM实现的内部需要可能引入额外开销。
    • ​元数据开销:​​ Record类本身在方法区(元空间)确实需要存储额外的RecordComponent信息,这部分是永久代/元空间的开销,但对单个堆对象实例的大小没有直接影响。间接地,它影响了记录类元数据的大小和访问模式。
    • ​访问速度:​​ 字段访问速度理论上应和传统Class一样,都是通过直接偏移量访问。Record并没有提供性能上的劣势。
  3. ​JVM实现的演进性:​​ Record是一个较新的特性。JVM(尤其是HotSpot)对其的实现和优化还在演进中。​​不同JDK版本(如JDK 16前后)、不同JVM实现、不同启动参数下的内存布局都可能存在差异。​​ 今天的优化点可能是明天的历史包袱。

对资深开发者的启示与实践建议

  1. ​性能敏感处,度量先行!​​ 永远不要仅仅基于“感觉”或“语法简洁”就在性能关键路径上大规模采用新技术(包括Record)。使用像JOL、Async Profiler、VisualVM、JMH这类工具进行​​实际测量和剖析​​,特别是当你处理海量对象时。关注对象的浅大小(Shallow Size)和保留大小(Retained Size)。
  2. ​理解Record的本质价值:​​ Record的核心优势在于​​开发效率、代码可读性、维护性和语义清晰度​​。对于绝大多数应用场景(如常见的DTO、配置项、领域值对象),这点额外的内存开销(即使存在)是完全可以接受的,其带来的好处远大于微小的空间代价。
  3. ​权衡点:字段数量和对齐敏感度:​
    • 如果Record包含​​大量字段​​(例如>8个int),那么单个实例上由于对齐填充导致的比例性浪费会相对减少,Record相对于手动编写等价的、可能也需要填充的Class,其优势可能会逐渐体现,或者至少差异缩小。
    • 对于​​极少量字段(特别是当总“核心”大小接近对齐边界时)​​,手动编写的Class有极小概率可以规避特定版本的JVM为Record引入的内部填充(如前所述的原因),从而在特定条件下节省几个字节。
  4. ​优先选用Record的场景:​​ 除非有极其严苛(并且经实际测量证实)的内存压力,否则在定义不可变数据载体时,​​Record应该作为首选方案​​。它能显著减少样板代码,提高代码健壮性(自动finalnull检查),并清晰地表达设计意图。
  5. ​谨慎手动优化的场景:​​ 只有当满足以下​​全部条件​​时,才考虑为极少量字段的情况手动编写Class并追求绝对最小内存占用:
    • ​该对象被数百万、甚至数亿级​​地实例化并常驻内存。
    • 通过JOL和堆分析工具​​确证Record版本的内存占用是瓶颈​​。
    • 手动编写的Class版本确实能​​稳定、显著地​​减少内存消耗(例如,从24B降到16B)。
    • 你能够并且​​愿意承担手动维护equalshashCodetoString、构造器等带来的长期维护成本和潜在错误风险​​。
    • 你能处理或忽略ClassicPoint在API易用性上的缺失。

结论

Java Record是一项提高生产力的伟大特性。它的首要目标是​​简化代码​​和​​增强语义​​。虽然它的命名“记录”(Record)和简洁语法容易让人联想到“轻量”,但正如我们的JOL探秘所揭示的,在HotSpot JVM的当前实现下,​​其单个实例的内存占用并不总是优于等效的手写Class​​,特别是在存在字段对齐和JVM内部实现细节影响的情况下。这种差异源于平台实现的优化决策(如JDK 16+为支持RecordComponent引入的元数据关联方式),而非Record本身的抽象成本。

因此,作为资深Java开发者,我们的认知需要​​从“Record必然省内存”升级为“Record优化了开发,其运行时成本需具体测量”​​。在需要极致内存优化的特定角落,我们要拿出工具箱(JOL、Profiler),进行基于数据的实证分析。而对于更广阔的应用场景,请继续拥抱Record带来的清晰和便捷——它的价值,远远超越了那几个潜在的字节差异。毕竟,代码是写给人看的,偶尔才是写给机器榨取极限性能的。明智的工程师懂得在性能与效率、清晰度和可维护性之间找到平衡点。


​附录(供实际博客中添加):​

  1. ​详细的JOL命令或代码示例:​​ 展示如何运行JOL生成上述分析。
  2. ​不同JDK版本的对比:​​ 简要说明JDK 16之前、JDK 16+的内存布局差异。
  3. ​关闭压缩指针的结果:​​ 演示关闭-XX:-UseCompressedOops后布局和大小变化。
  4. ​包含引用类型字段的Record分析:​​ 例如record Person(String name, int age),分析引用带来的开销。
  5. ​JMH微基准测试代码片段:​​ 对比PointClassicPoint的创建速度、访问字段速度,通常差别不大(或Record略快?),但可以量化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/909495.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue 3 九宫格抽奖系统,采用优雅的 UI 设计和流畅的动画效果

九宫格抽奖 预览地址 项目简介 这是一个基于 Vue 3 开发的现代化九宫格抽奖系统,采用优雅的 UI 设计和流畅的动画效果,为用户提供极致的抽奖体验。系统支持多种奖品配置,实时抽奖记录展示,以及完整的活动说明功能。 核心功能 …

无缝对接大疆算力平台:基于Coovally的无人机AI模型端到端优化方案

【导读】 随着无人机应用场景的快速拓展,企业对于定制化AI解决方案的需求日益迫切。大疆算力开放平台为开发者提供了专业的模型量化与部署环境,帮助开发者将训练好的AI模型高效部署至大疆无人机平台。 然而,要实现完整的AI开发闭环&#xf…

ubuntu下载CUDA cuDNN

nivida-smi查看显卡驱动版本 (一)安装CUDA cuda官网 cuda官网 下载对应版本的cuda 这个官网真不错啊,下面附上了指令 wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-ubuntu2404.pin sudo mv c…

FreeRTOS定时器

目录 1.特性2.运行环境2.1 守护任务2.2 回调函数2.3 内部源码 3.和Linux对比4.ID5.数据传输6.操作函数6.1 创建6.2 删除6.3 启动6.4 停止6.5 复位(重置)6.6 修改周期6.7 注意事项 7.示例:一般使用8.示例:定时器防抖 1.特性 定时器…

JavaScript中的迭代器模式:优雅遍历数据的“设计之道”

JavaScript中的迭代器模式:优雅遍历数据的“设计之道” 一、什么是迭代器模式? 在编程世界中,迭代器模式(Iterator Pattern)是一种经典的设计模式,它的核心思想是:为集合对象提供一种统一的访…

Debian/Ubuntu systemd coredump调试程序Crash

程序是通过systemd监管,当程序出现crash的时候,需要保存crash的日志,也就是coredump日志,按照一般做法设置coredump。而在安装有systemd服务的系统中一般都有systemd-coredump服务。 systemd-coredump 是 systemd 子系统中的一个工…

【图片转 3D 模型】北大·字节跳动·CMU携手——单图15 秒生成结构化3D模型!

​​引言:单图生成结构化 3 D 模型的技术突破​ ​ PartCrafter 由北京大学、字节跳动与卡耐基梅隆大学联合研发,是全球首个​​端到端生成结构化 3 D 网格​​的模型。它仅需单张 RGB 图像,即可在 34 秒内生成带语义分解的 3 D 部件&#xf…

零基础RT-thread第二节:按键控制

我这里依然使用的是野火开发板,F767芯片。 这一节写一下按键控制LED亮灭。 这是按键以及LED的原理图。 按键对应的引脚不按下时是低电平,按下后是高电平。 LED是在低电平点亮。 接下来是key.c: /** Copyright (c) 2006-2021, RT-Thread Development T…

《Gulp与SCSS:解构前端样式开发的底层逻辑与实战智慧》

探寻Gulp与SCSS协作的底层逻辑 Gulp,作为任务自动化的佼佼者,其核心价值在于将一系列复杂的任务,如文件的编译、合并、压缩等,以一种流畅且高效的方式串联起来,形成一个自动化的工作流。它基于流(stream&a…

OpenCV CUDA模块图像变形------对图像进行GPU加速的透视变换函数warpPerspective()

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 该函数用于对图像进行 GPU 加速的透视变换(Perspective Transformation),是 cv::warpPerspective 的 CUDA 版…

吴恩达机器学习笔记(2)—单变量线性回归

目录 一、模型表示 二、代价函数 三、代价函数的直观理解(1) 四、代价函数的直观理解(2) 五、梯度下降 六、梯度下降的直观理解 七、线性回归的梯度下降 在本篇内容中,我们将介绍第一个机器学习算法——线性回归…

最新华为 HCIP-Datacom(H12-821)

最新 HCIP-Datacom(H12-821),完整题库请上方访问,更新完毕。 在OSPF网络中,NSSA区域与STUB区域都是为了减少LSA数量,两者最主要的区别在于,NSSA区域可以引入外部路由,并同时接收OSPF…

vba学习系列(11)--批退率通过率等数据分析

系列文章目录 文章目录 系列文章目录前言一、外观报表1.产能2.固定伤排查3.镜片不良TOP4.镜片公式计算5.镜片良率计算6.镜片批退率7.镜筒不良TOP8.镜筒公式计算9.镜筒良率计算10.镜筒批退率 二、反射率报表1.机台通过率2.镜片通过率圈数分析3.镜片通过率罩次分析4.镜筒通过率圈…

成功在 Conda Python 2.7 环境中安装 Clipper(eCLIP peak caller)

🔬 成功在 Conda Python 2.7 环境中安装 Clipper(eCLIP peak caller) 本文记录了如何在无 root 权限下使用 Conda 环境,解决依赖、构建扩展模块并成功安装运行 clipper 的详细流程。适用于再现 eCLIP 分析流程时遇到 clipper 安装…

通过 VS Code 连接 GitLab 并上传项目

通过 VS Code 连接 GitLab 并上传项目,请按照以下步骤操作: 1. 安装必要工具 确保已安装 Git 并配置用户名和邮箱: git config --global user.name "你的用户名" git config --global user.email "你的邮箱" 在 VS Cod…

开源夜莺支持MySQL数据源,更方便做业务指标监控了

夜莺监控项目最核心的定位,是做一个告警引擎,支持多种数据源的告警。这个版本的更新主要是增加了对 MySQL 数据源的支持,进一步增强了夜莺在业务指标监控方面的能力。 之前版本的夜莺主要聚焦在 Prometheus、VictoriaMetrics、ElasticSearch…

SpringCloud + MybatisPlus:多租户模式与实现

一、多租户的基本概念 多租户(Multi-Tenancy) 是指在一套软件系统中,多个租户(客户)共享相同的基础设施和应用程序,但数据和配置相互隔离的架构模式。其核心目标是 降低成本 和 保证数据安全。 核心特点: 资源共享:租户共享服务器、数据库、代码等资源。数据隔离:通…

Kafka入门:解锁核心组件,开启消息队列之旅

一、引言 Kafka以超高速吞吐、精准的路由策略和永不掉线的可靠性,让海量数据在分布式系统中畅行无阻。无论你是刚接触消息队列的技术小白,还是寻求性能突破的开发老手,掌握 Kafka 核心组件的运作原理,都是解锁高效数据处理的关键…

前端项目Excel数据导出同时出现中英文表头错乱情况解决方案。

文章目录 前言一、Excel导出出现中英文情况。二、解决方案数据处理 三、效果展示总结 前言 在前端项目中实现Excel导出功能时,数据导出excel是常见的业务需求。但excel导出完表头同时包含了中文和英文的bug,下面是我的经验分享,应该可以帮助…

《开窍》读书笔记8

51.学会赞美他人,能净化心灵,建立良好人际关系,让生活充满阳光。 52.欣赏他人的学习过程,能激发潜能,促进相互成长,让有点共存。 53.别因“自我”一叶障目,要关注他人,欣赏与别欣赏式…