優化應用程式代碼的記憶體使用並非新話題,但往往未被深入理解。本文將從 Java 進程記憶體模型切入,深入剖析 Java 物件與集合在堆中的真實佔用,並給出提升記憶體效率的實戰策略,重點聚焦 HashMap、ArrayList 等核心集合的優化。
啟動 Java 應用時,JVM 建立一個作業系統進程。受處理器位數限制:32 位可址空間 4GB,64 位理論達 16 EB。作業系統核心與 C 執行環境佔用部分空間(Windows 預設 2GB),剩餘為用戶空間,供 Java 進程使用,劃分為兩大池:
-Xms/-Xmx 控制,存放 Java 物件。32 位進程記憶體佈局示意:

new Integer(42) 看似只存一個 int,實際佔用遠超 32 位。JVM 為每個物件附加中繼資料(物件頭),典型包含:
java.lang.Integer)。緊接著是實例欄位資料(Integer 即一個 int)。
32 位 JVM 下 Integer 物件佈局:

共 128 位(16 位元組),其中 32 位存資料,96 位為中繼資料,開銷比 3:1。
64 位 JVM 下指標與鎖擴為 64 位,Integer 達 224 位(28 位元組),開銷比 7:1;單元素 int[] 達 288 位,開銷比 9:1。

應用從 32 位遷移至 64 位,Java 堆通常膨脹 70% 左右(1GB → 1.7GB),本機堆甚至增 90%。
IBM JVM -Xcompressedrefs、Oracle JVM -XX:+UseCompressedOops 可將 64 位模式下的物件指標壓縮為 32 位,基本消除 Java 堆膨脹,但本機堆仍偏高。

int[] 除標準物件頭外,額外含「長度」欄位。32 位下單元素 int[] 佔 160 位,開銷比 5:1。

String 封裝 char[],含雜湊值、長度、偏移量、陣列引用等欄位。8 字元字串(128 位資料)實際佔 480 位(60 位元組),開銷比 3.75:1。

資料結構越複雜,間接引用越多,開銷疊加越嚴重。
核心集合功能越強、開銷越大。常用集合按功能與開銷遞減:HashSet、HashMap、Hashtable、LinkedList、ArrayList。(HashSet 實為包裝 HashMap,功能較少但略大。)
HashMap 內部為 HashMap$Entry 陣列,預設容量 16。空 HashMap 佔 128 位元組。每個鍵值對封裝於 Entry 物件(32 位元組,含 keyHash、next、key、value)。

總開銷公式:
HashMap物件 + 陣列開銷 + 條目數 × (Entry陣列槽 + Entry物件)
10,000 條目僅結構開銷約 360KB,未計鍵值本身大小。

結構類似 HashMap,預設容量 11,空表 104 位元組。Entry 同 32 位元組。主要差異:不接受 null、預設同步(可能損效能),可用 Collections.synchronizedMap() 替代。


空表 48 位元組(LinkedList 24B + Entry 24B)。Entry 含 previous、next、value,無雜湊值,單條目開銷 24 位元組,低於 Hash 類。但查找為 O(n),大量資料效能差。


空表 88 位元組(ArrayList 32B + 預設長度 10 的 Object[] 56B)。單條目僅一個引用(4/8 位元組),記憶體效率最高。但擴容機制(通常 1.5 倍)導致填充率波動。


實為 char[],預設容量 16,空佔 72 位元組。


集合鮮能「剛好滿」。預設容量與實際資料量差距造成額外開銷。
例:StringBuffer 存 "MY STRING"(9 字元),容量 16,7 個空槽浪費 112 位元組,填充率 0.56。

追加 " OF TEXT" 觸發擴容至 32,實際 17 字元,填充率 0.53,浪費 240 位元組。

大規模時極為可觀:16MB 資料的 StringBuffer 實佔 32MB 陣列,浪費 16MB。

ArrayList 單條目開銷遠低於 LinkedList,但非精確調整。長度可預測用 ArrayList(指定初始容量),不可預測用 LinkedList 減少空洞。new ArrayList(expectedSize))+ 避免過度擴容 = 最大化填充率,最小化記憶體佔用。負載測試中 206MB 堆中,集合本身佔 42.9MB(21%),超過 30 萬集合實例。優化集合類型與大小可觀節省記憶體。

IBM Memory Analyzer (MAT) 提供集合填充率分析:

選擇 Collection Fill Ratio 查詢,設定物件類型(如 java.util.Hashtable)、分段數。結果顯示 262,234 個 Hashtable 中 48.4% 為空,幾乎全為少量條目。

右鍵 list objects → with incoming references 追溯持有者。發現空 Hashtable 歸 javax.management.remote.rmi.NoCallStackClassLoader 所有。


優化方向:延遲分配、精確初始容量。
統計顯示平均 50%+ 集合為空。

WeakHashMap 內含 ReferenceQueue,空時仍佔 560 位元組。19,465 空實例額外耗 10.9MB。AS 8 / Java 7 將 ReferenceQueue 內部陣列改為延遲分配,僅在加入引用時建立。

複雜應用中集合數量驚人。設計開發階段選對集合、設好初始容量、考慮延遲初始化,能帶來可觀記憶體節省。對既有系統,善用 Memory Analyzer 定位低填充率集合,精準優化。