深入探究其內存開銷與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 x??4 bytes4 bytes記錄第一個字段x
??字段 int y??4 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
繁體地址,請注明出處:http://hk.pswp.cn/news/909495.shtml
英文地址,請注明出處:http://en.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.別因“自我”一葉障目,要關注他人,欣賞與別欣賞式…