从Java代码到Java堆理解和优化您的应用程序的内存使用
從Java代碼到Java堆理解和優化您的應用程序的內存使用
簡介: 本文將為您提供 Java? 代碼內存使用情況的深入見解,包括將 int 值置入一個Integer 對象的內存開銷、對象委托的成本和不同集合類型的內存效率。您將了解到如何確定應用程序中的哪些位置效率低下,以及如何選擇正確的集合來改進您的代碼。優化應用程序代碼的內存使用并不是一個新主題,但是人們通常并沒有很好地理解這個主題。本文將簡要介紹 Java 進程的內存使用,隨后深入探討您編寫的 Java 代碼的內存使用。最后,本文將展示提高代碼內存效率的方法,特別強調了HashMap 和 ArrayList等 Java 集合的使用。
背景信息:Java 進程的內存使用
通過在命令行中執行java 或者啟動某種基于 Java 的中間件來運行 Java 應用程序時,Java 運行時會創建一個操作系統進程,就像您運行基于 C 的程序時那樣。實際上,大多數 JVM 都是用 C 或者 C++ 語言編寫的。作為操作系統進程,Java 運行時面臨著與其他進程完全相同的內存限制:架構提供的尋址能力以及操作系統提供的用戶空間。
架構提供的內存尋址能力依賴于處理器的位數,舉例來說,32 位或者 64 位,對于大型機來說,還有 31 位。進程能夠處理的位數決定了處理器能尋址的內存范圍:32 位提供了 2^32 的可尋址范圍,也就是 4,294,967,296 位,或者說 4GB。而 64 位處理器的可尋址范圍明顯增大:2^64,也就是 18,446,744,073,709,551,616,或者說 16 exabyte(百億億字節)。通過在命令行中執行 java 或者啟動某種基于 Java 的中間件來運行 Java 應用程序時,Java 運行時會創建一個操作系統進程,就像您運行基于 C 的程序時那樣。實際上,大多數 JVM 都是用 C 或者 C++ 語言編寫的。作為操作系統進程,Java 運行時面臨著與其他進程完全相同的內存限制:架構提供的尋址能力以及操作系統提供的用戶空間。
處理器架構提供的部分可尋址范圍由 OS 本身占用,提供給操作系統內核以及 C 運行時(對于使用 C 或者 C++ 編寫的 JVM 而言)。OS 和 C 運行時占用的內存數量取決于所用的 OS,但通常數量較大:Windows 默認占用的內存是 2GB。剩余的可尋址空間(用術語來表示就是用戶空間)就是可供運行的實際進程使用的內存。
對于 Java 應用程序,用戶空間是 Java 進程占用的內存,實際上包含兩個池:Java 堆和本機(非 Java)堆。Java 堆的大小由 JVM 的 Java 堆設置控制:-Xms 和-Xmx 分別設置最小和最大 Java 堆。在按照最大的大小設置分配了 Java 堆之后,剩下的用戶空間就是本機堆。圖 1 展示了一個 32 位 Java 進程的內存布局:
圖 1. 一個 32 位 Java 進程的內存布局示例
在 圖 1 中,可尋址范圍總共有 4GB,OS 和 C 運行時大約占用了其中的 1GB,Java 堆占用了將近 2GB,本機堆占用了其他部分。請注意,JVM 本身也要占用內存,就像 OS 內核和 C 運行時一樣,而 JVB 占用的內存是本機堆的子集。
回頁首
Java 對象詳解
在您的 Java 代碼使用 new 操作符創建一個 Java 對象的實例時,實際上分配的數據要比您想的多得多。例如,一個 int 值與一個Integer 對象(能包含 int 值的最小對象)的大小比率是 1:4,這個比率可能會讓您感到吃驚。額外的開銷源于 JVM 用于描述 Java 對象的元數據,在本例中也就是 Integer。
根據 JVM 的版本和供應的不同,對象元數據的數量也各有不同,但其中通常包括:
- 類:一個指向類信息的指針,描述了對象類型。舉例來說,對于 java.lang.Integer 對象,這是 java.lang.Integer 類的一個指針。
- 標記:一組標記,描述了對象的狀態,包括對象的散列碼(如果有),以及對象的形狀(也就是說,對象是否是數組)。
- 鎖:對象的同步信息,也就是說,對象目前是否正在同步。
對象元數據后緊跟著對象數據本身,包括對象實例中存儲的字段。對于 java.lang.Integer 對象,這就是一個 int。
如果您正在運行一個 32 位 JVM,那么在創建 java.lang.Integer 對象實例時,對象的布局可能如圖 2 所示:
圖 2. 一個 32 位 Java 進程的 java.lang.Integer 對象的布局示例
如 圖 2 所示,有 128 位的數據用于存儲int 值內的 32 位數據,而對象元數據占用了其余 128 位。
回頁首
Java 數組對象詳解
數組對象(例如一個 int 值數組)的形狀和結構與標準 Java 對象相似。主要差別在于數組對象包含說明數組大小的額外元數據。因此,數據對象的元數據包括:
- 類:一個指向類信息的指針,描述了對象類型。舉例來說,對于 int 字段數組,這是 int[] 類的一個指針。
- 標記:一組標記,描述了對象的狀態,包括對象的散列碼(如果有),以及對象的形狀(也就是說,對象是否是數組)。
- 鎖:對象的同步信息,也就是說,對象目前是否正在同步。
- 大小:數組的大小。
圖 3 展示了一個 int 數組對象的布局示例:
圖 3. 一個 32 位 Java 進程的 int 數組對象的布局示例
如 圖 3 所示,有 160 位的數據用于存儲int 值內的 32 位數據,而數組元數據占用了其余 160 位。對于byte、int 和long 等原語,從內存的方面考慮,單項數組比對應的針對單一字段的包裝器對象(Byte、Integer 或Long)的成本更高。
回頁首
更為復雜數據結構詳解
良好的面向對象設計與編程鼓勵使用封裝(提供接口類來控制數據訪問)和委托(使用 helper 對象來實施任務)。封裝和委托會使大多數數據結構的表示形式中包含多個對象。一個簡單的示例就是java.lang.String 對象。java.lang.String 對象中的數據是一個字符數組,由管理和控制對字符數組的訪問的java.lang.String 對象封裝。圖 4 展示了一個 32 位 Java 進程的java.lang.String 對象的布局示例:
圖 4. 一個 32 位 Java 進程的 java.lang.String 對象的布局示例
如 圖 4 所示,除了標準對象元數據之外,java.lang.String 對象還包含一些用于管理字符串數據的字段。通常情況下,這些字段是散列值、字符串大小計數、字符串數據偏移量和對于字符數組本身的對象引用。
這也就意味著,對于一個 8 個字符的字符串(128 位的 char 數據),需要有 256 位的數據用于字符數組,224 位的數據用于管理該數組的 java.lang.String 對象,因此為了表示 128 位(16 個字節)的數據,總共需要占用 480 位(60 字節)。開銷比例為 3.75:1。
總體而言,數據結構越是復雜,開銷就越高。下一節將具體討論相關內容。
回頁首
32 位和 64 位 Java 對象
之前的示例中的對象大小和開銷適用于 32 位 Java 進程。在 背景信息:Java 進程的內存使用 一節中提到,64 位處理器的內存可尋址能力比 32 位處理器高得多。對于 64 位進程,Java 對象中的某些數據字段的大小(特別是對象元數據或者表示另一個對象的任何字段)也需要增加到 64 位。其他數據字段類型(例如int、byte 和long )的大小不會更改。圖 5 展示了一個 64 位 Integer 對象和一個 int 數組的布局:
圖 5. 一個 64 位進程的 java.lang.Integer 對象和int 數組的布局示例
圖 5 表明,對于一個 64 位Integer 對象,現在有 224 位的數據用于存儲 int 字段所用的 32 位,開銷比例是 7:1。對于一個 64 位單元素 int 數組,有 288 位的數據用于存儲 32 位 int 條目,開銷比例是 9:1。這在實際應用程序中產生的影響在于,之前在 32 位 Java 運行時中運行的應用程序若遷移到 64 位 Java 運行時,其 Java 堆內存使用量會顯著增加。通常情況下,增加的數量是原始堆大小的 70% 左右。舉例來說,一個在 32 位 Java 運行時中使用 1GB Java 堆的 Java 應用程序在遷移到 64 位 Java 運行時之后,通常需要使用 1.7GB 的 Java 堆。
請注意,這種內存增加并非僅限于 Java 堆。本機堆內存區使用量也會增加,有時甚至要增加 90% 之多。
表 1 展示了一個應用程序在 32 位和 64 位模式下運行時的對象和數組字段大小:
表 1. 32 位和 64 位 Java 運行時的對象中的字段大小
| boolean | 32 | 32 | 8 | 8 |
| byte | 32 | 32 | 8 | 8 |
| char | 32 | 32 | 16 | 16 |
| short | 32 | 32 | 16 | 16 |
| int | 32 | 32 | 32 | 32 |
| float | 32 | 32 | 32 | 32 |
| long | 32 | 32 | 64 | 64 |
| double | 32 | 32 | 64 | 64 |
| 對象字段 | 32 | 64 (32*) | 32 | 64 (32*) |
| 對象元數據 | 32 | 64 (32*) | 32 | 64 (32*) |
* 對象字段的大小以及用于各對象元數據條目的數據的大小可通過 壓縮引用或壓縮 OOP 技術減小到 32 位。
壓縮引用和壓縮普通對象指針 (OOP)
IBM 和 Oracle JVM 分別通過壓縮引用 (-Xcompressedrefs) 和壓縮 OOP (-XX:+UseCompressedOops) 選項提供對象引用壓縮功能。利用這些選項,即可在 32 位(而非 64 位)中存儲對象字段和對象元數據值。在應用程序從 32 位 Java 運行時遷移到 64 位 Java 運行時的時候,這能消除 Java 堆內存使用量增加 70% 的負面影響。請注意,這些選項對于本機堆的內存使用無效,本機堆在 64 位 Java 運行時中的內存使用量仍然比 32 位 Java 運行時中的使用量高得多。
回頁首
Java 集合的內存使用
在大多數應用程序中,大量數據都是使用核心 Java API 提供的標準 Java Collections 類來存儲和管理的。如果內存占用對于您的應用程序極為重要,那么就非常有必要了解各集合提供的功能以及相關的內存開銷。總體而言,集合功能的級別越高,內存開銷就越高,因此使用提供的功能多于您需要的功能的集合類型會帶來不必要的額外內存開銷。
其中部分最常用的集合如下:
- HashSet
- HashMap
- Hashtable
- LinkedList
- ArrayList
除了 HashSet 之外,此列表是按功能和內存開銷進行降序排列的。(HashSet 是包圍一個HashMap 對象的包裝器,它提供的功能比HashMap 少,同時容量稍微小一些。)
Java 集合:HashSet
HashSet 是Set 接口的實現。Java Platform SE 6 API 文檔對于HashSet 的描述如下:
一個不包含重復元素的集合。更正式地來說,set(集)不包含元素 e1 和 e2 的配對 e1.equals(e2),而且至多包含一個空元素。正如其名稱所表示的那樣,這個接口將建模數學集抽象。HashSet 包含的功能比HashMap 要少,只能包含一個空條目,而且無法包含重復條目。該實現是包圍HashMap 的一個包裝器,以及管理可在 HashMap 對象中存放哪些內容的 HashSet 對象。限制HashMap 功能的附加功能表示 HashSet 的內存開銷略高。
圖 6 展示了 32 位 Java 運行時中的一個 HashSet 的布局和內存使用:
圖 6. 32 位 Java 運行時中的一個 HashSet 的內存使用和布局
圖 6 展示了一個java.util.HashSet 對象的 shallow 堆(獨立對象的內存使用)以及保留堆(獨立對象及其子對象的內存使用),以字節為單位。shallow 堆的大小是 16 字節,保留堆的大小是 144 字節。創建一個HashSet 時,其默認容量(也就是該集中可以容納的條目數量)將設置為 16 個條目。按照默認容量創建HashSet,而且未在該集中輸入任何條目時,它將占用 144 個字節。與HashMap 的內存使用相比,超出了 16 個字節。表 2 顯示了HashSet 的屬性:
表 2. 一個 HashSet 的屬性
| 16 個條目 |
| 144 個字節 |
| 16 字節加 HashMap 開銷 |
| 16 字節加 HashMap 開銷 |
| O(1):所用時間是一個常量時間,無論要素數量如何都是如此(假設無散列沖突) |
Java 集合:HashMap
HashMap 是Map 接口的實現。Java Platform SE 6 API 文檔對于HashMap 的描述如下:
一個將鍵映射到值的對象。一個映射中不能包含重復的鍵;每個鍵僅可映射到至多一個值。HashMap 提供了一種存儲鍵/值對的方法,使用散列函數將鍵轉換為存儲鍵/值對的集合中的索引。這允許快速訪問數據位置。允許存在空條目和重復條目;因此,HashMap 是HashSet 的簡化版。
HashMap 將實現為一個HashMap$Entry 對象數組。圖 7 展示了 32 位 Java 運行時中的一個HashMap 的內存使用和布局:
圖 7. 32 位 Java 運行時中的一個 HashMap 的內存使用和布局
如 圖 7 所示,創建一個HashMap 時,結果是一個 HashMap 對象以及一個采用 16 個條目的默認容量的 HashMap$Entry 對象數組。這提供了一個HashMap,在完全為空時,其大小是 128 字節。插入 HashMap 的任何鍵/值對都將包含于一個 HashMap$Entry 對象之中,該對象本身也有一定的開銷。
大多數 HashMap$Entry 對象實現都包含以下字段:
- int KeyHash
- Object next
- Object key
- Object value
一個 32 字節的 HashMap$Entry 對象用于管理插入集合的數據鍵/值對。這就意味著,一個HashMap 的總開銷包含 HashMap 對象、一個HashMap$Entry 數組條目和與各條目對應的HashMap$Entry 對象的開銷。可通過以下公式表示:
HashMap 對象 + 數組對象開銷 + (條目數量 * (HashMap$Entry 數組條目 +HashMap$Entry 對象))對于一個包含 10,000 個條目的 HashMap 來說,僅僅 HashMap、HashMap$Entry 數組和HashMap$Entry 對象的開銷就在 360K 左右。這還沒有考慮所存儲的鍵和值的大小。
表 3 展示了 HashMap 的屬性:
表 3. 一個 HashMap 的屬性
| 16 個條目 |
| 128 個字節 |
| 64 字節加上每個條目 36 字節 |
| ~360K |
| O(1):所用時間是一個常量時間,無論要素數量如何都是如此(假設無散列沖突) |
Java 集合:Hashtable
Hashtable 與HashMap 相似,也是 Map 接口的實現。Java Platform SE 6 API 文檔對于 Hashtable 的描述如下:
這個類實現了一個散列表,用于將鍵映射到值。對于非空對象,可以將它用作鍵,也可以將它用作值。Hashtable 與HashMap 極其相似,但有兩項限制。無論是鍵還是值條目,它均不接受空值,而且它是一個同步集合。相比之下,HashMap 可以接受空值,且不是同步的,但可以利用Collections.synchronizedMap() 方法來實現同步。
Hashtable 的實現同樣類似于 HashMap,也是條目對象的數組,在本例中即 Hashtable$Entry 對象。圖 8 展示了 32 位 Java 運行時中的一個 Hashtable 的內存使用和布局:
圖 8. 32 位 Java 運行時中的一個 Hashtable 的內存使用和布局
圖 8 顯示,創建一個Hashtable 時,結果會是一個占用了 40 字節的內存的 Hashtable 對象,另有一個默認容量為 11 個條目的Hashtable$entry 數組,在Hashtable 為空時,總大小為 104 字節。
Hashtable$Entry 存儲的數據實際上與HashMap 相同:
- int KeyHash
- Object next
- Object key
- Object value
這意味著,對于 Hashtable 中的鍵/值條目,Hashtable$Entry 對象也是 32 字節,而Hashtable 開銷的計算和 10K 個條目的集合的大小(約為 360K)與HashMap 類似。
表 4 顯示了 Hashtable 的屬性:
表 4. 一個 Hashtable 的屬性
| 11 個條目 |
| 104 個字節 |
| 56 字節加上每個條目 36 字節 |
| ~360K |
| O(1):所用時間是一個常量時間,無論要素數量如何都是如此(假設無散列沖突) |
如您所見,Hashtable 的默認容量比HashMap 要稍微小一些(分別是 11 與 16)。除此之外,兩者之間的主要差別在于Hashtable 無法接受空鍵和空值,而且是默認同步的,但這可能是不必要的,還有可能降低集合的性能。
Java 集合:LinkedList
LinkedList 是List 接口的鏈表實現。Java Platform SE 6 API 文檔對于LinkedList 的描述如下:
一種有序集合(也稱為序列)。此接口的用戶可以精確控制將各元素插入列表時的位置。用戶可以按照整數索引(代表在列表中的位置)來訪問元素,也可以搜索列表中的元素。與其他集合 (set) 不同,該集合 (collection) 通常允許存在重復的元素。實現是 LinkedList$Entry 對象鏈表。圖 9 展示了 32 位 Java 運行時中的LinkedList 的內存使用和布局:
圖 9. 32 位 Java 運行時中的一個 LinkedList 的內存使用和布局
圖 9 表明,創建一個LinkedList 時,結果將得到一個占用 24 字節內存的 LinkedList 對象以及一個 LinkedList$Entry 對象,在LinkedList 為空時,總共占用的內存是 48 個字節。
鏈表的優勢之一就是能夠準確調整其大小,且無需重新調整。默認容量實際上就是一個條目,能夠在添加或刪除條目時動態擴大或縮小。每個LinkedList$Entry 對象仍然有自己的開銷,其數據字段如下:
- Object previous
- Object next
- Object value
但這比 HashMap 和Hashtable 的開銷低,因為鏈表僅存儲單獨一個條目,而非鍵/值對,由于不會使用基于數組的查找,因此不需要存儲散列值。從負面角度來看,在鏈表中查找的速度要慢得多,因為鏈表必須依次遍歷才能找到需要查找的正確條目。對于較大的鏈表,結果可能導致漫長的查找時間。
表 5 顯示了 LinkedList 的屬性:
表 5. 一個 LinkedList 的屬性
| 1 個條目 |
| 48 個字節 |
| 24 字節加上每個條目 24 字節 |
| ~240K |
| O(n):所用時間與元素數量線性相關。 |
Java 集合:ArrayList
ArrayList 是List 接口的可變長數組實現。Java Platform SE 6 API 文檔對于ArrayList 的描述如下:
一種有序集合(也稱為序列)。此接口的用戶可以精確控制將各元素插入列表時的位置。用戶可以按照整數索引(代表在列表中的位置)來訪問元素,也可以搜索列表中的元素。與其他集合 (set) 不同,該集合 (collection) 通常允許存在重復的元素。不同于 LinkedList,ArrayList 是使用一個Object 數組實現的。圖 10 展示了一個 32 位 Java 運行時中的ArrayList 的內存使用和布局:
圖 10. 32 位 Java 運行時中的一個 ArrayList 的內存使用和布局
圖 10 表明,在創建ArrayList 時,結果將得到一個占用 32 字節內存的 ArrayList 對象,以及一個默認大小為 10 的 Object 數組,在ArrayList 為空時,總計占用的內存是 88 字節。這意味著 ArrayList 無法準確調整大小,因此擁有一個默認容量,恰好是 10 個條目。
表 6 展示了一個 ArrayList 的屬性:
表 6. 一個 ArrayList 的屬性
| 10 |
| 88 個字節 |
| 48 字節加上每個條目 4 字節 |
| ~40K |
| O(n):所用時間與元素數量線性相關 |
其他類型的 “集合”
除了標準集合之外,StringBuffer 也可以視為集合,因為它管理字符數據,而且在結構和功能上與其他集合相似。Java Platform SE 6 API 文檔對于StringBuffer 的描述如下:
線程安全、可變的字符序列……每個字符串緩沖區都有相應的容量。只要字符串緩沖區內包含的字符序列的長度不超過容量,就不必分配新的內部緩沖區數組。如果內部緩沖區溢出,則會自動為其擴大容量。StringBuffer 是作為一個char 數組來實現的。圖 11 展示了一個 32 位 Java 運行時中的StringBuffer 的內存使用和布局:
圖 11. 32 位 Java 運行時中的一個 StringBuffer 的內存使用和布局
圖 11 展示,創建一個StringBuffer 時,結果將得到一個占用 24 字節內存的 StringBuffer 對象,以及一個默認大小為 16 的字符數組,在 StringBuffer 為空時,數據總大小為 72 字節。
與集合相似,StringBuffer 擁有默認容量和重新調整大小的機制。表 7 顯示了StringBuffer 的屬性:
表 7. 一個 StringBuffer 的屬性
| 16 |
| 72 個字節 |
| 24 個字節 |
| 24 個字節 |
| 不適用 |
回頁首
集合中的空白空間
擁有給定數量對象的各種集合的開銷并不是內存開銷的全部。前文的示例中的度量假設集合已經得到了準確的大小調整。然而,對于大多數集合來說,這種假設都是不成立的。大多數集合在創建時都指定給定的初始容量,數據將置入集合之中。這也就是說,集合擁有的容量往往大于集合中存儲的數據容量,這造成了額外的開銷。
考慮一個 StringBuffer 的示例。其默認容量是 16 個字符條目,大小為 72 字節。初始情況下,72 個字節中未存儲任何數據。如果您在字符數組中存儲了一些字符,例如"MY STRING" ,那么也就是在 16 個字符的數組中存儲了 9 個字符。圖 12 展示了 32 位 Java 運行時中的一個包含"MY STRING" 的 StringBuffer 的內存使用和布局:
圖 12. 32 位 Java 運行時中的一個包含 "MY STRING" 的StringBuffer 的內存使用
如 圖 12 所示,數組中有 7 個可用的字符條目未被使用,但占用了內存,在本例中,這造成了 112 字節的額外開銷。對于這個集合,您在 16 的容量中存儲了 9 個條目,因而填充率 為 0.56。集合的填充率越低,因多余容量而造成的開銷就越高。
回頁首
集合的擴展和重新調整
在集合達到容量限制時,如果出現了在集合中存儲額外條目的請求,那么會重新調整集合,并擴展它以容納新條目。這將增加容量,但往往會降低填充比,造成更高的內存開銷。
各集合所用的擴展算法各有不同,但一種通用的做法就是將集合的容量加倍。這也是 StringBuffer 采用的方法。對于前文示例中的StringBuffer,如果您希望將" OF TEXT" 添加到緩沖區中,生成 "MY STRING OF TEXT",則需要擴展集合,因為新的字符集合擁有 17 個條目,當前容量 16 無法滿足其要求。圖 13 展示了所得到的內存使用:
圖 13. 32 位 Java 運行時中的一個包含 "MY STRING OF TEXT" 的StringBuffer 的內存使用
現在,如 圖 13 所示,您得到了一個 32 個條目的字符數組,但僅僅使用了 17 個條目,填充率為 0.53。填充率并未顯著下滑,但您現在需要為多余的容量付出 240 字節的開銷。
對于小字符串和集合,低填充率和多余容量的開銷可能并不會被視為嚴重問題,而在大小增加時,這樣的問題就會愈加明顯,代價也就愈加高昂。例如,如果您創建了一個StringBuffer,其中僅包含 16MB 的數據,那么(在默認情況下)它將使用大小設置為可容納 32MB 數據的字符數組,這造成了以多余容量形式存在的 16MB 的額外開銷。
回頁首
Java 集合:匯總
表 8 匯總了集合的屬性:
表 8. 集合屬性匯總
| HashSet | O(1) | 16 | 144 | 360K | 否 | x2 |
| HashMap | O(1) | 16 | 128 | 360K | 否 | x2 |
| Hashtable | O(1) | 11 | 104 | 360K | 否 | x2+1 |
| LinkedList | O(n) | 1 | 48 | 240K | 是 | +1 |
| ArrayList | O(n) | 10 | 88 | 40K | 否 | x1.5 |
| StringBuffer | O(1) | 16 | 72 | 24 | 否 | x2 |
Hash 集合的性能比任何List 的性能都要高,但每條目的成本也要更高。由于訪問性能方面的原因,如果您正在創建大集合(例如,用于實現緩存),那么最好使用基于Hash 的集合,而不必考慮額外的開銷。
對于并不那么注重訪問性能的較小集合而言,List 則是合理的選擇。ArrayList 和LinkedList 集合的性能大體相同,但其內存占用完全不同:ArrayList 的每條目大小要比LinkedList 小得多,但它不是準確設置大小的。List 要使用的正確實現是ArrayList 還是LinkedList 取決于List 長度的可預測性。如果長度未知,那么正確的選擇可能是 LinkedList,因為集合包含的空白空間更少。如果大小已知,那么 ArrayList 的內存開銷會更低一些。
選擇正確的集合類型使您能夠在集合性能與內存占用之間達到合理的平衡。除此之外,您可以通過正確調整集合大小來最大化填充率、最小化未得到利用的空間,從而最大限度地減少內存占用。
回頁首
集合的實際應用:PlantsByWebSphere 和 WebSphere Application Server Version 7
在 表 8 中,創建一個包含 10,000 個條目、基于Hash 的集合的開銷是 360K。考慮到,復雜的 Java 應用程序常常使用大小為數 GB 的 Java 堆運行,因此這樣的開銷看起來并不是非常高,當然,除非使用了大量集合。
表 9 展示了在包含五個用戶的負載測試中運行 WebSphere? Application Server Version 7 提供的 PlantsByWebSphere 樣例應用程序時,Java 堆使用的 206MB 中的集合對象使用量:
表 9. WebSphere Application Server v7 中的 PlantsByWebSphere 的集合使用量
| Hashtable | 262,234 | 26.5 |
| WeakHashMap | 19,562 | 12.6 |
| HashMap | 10,600 | 2.3 |
| ArrayList | 9,530 | 0.3 |
| HashSet | 1,551 | 1.0 |
| Vector | 1,271 | 0.04 |
| LinkedList | 1,148 | 0.1 |
| TreeMap | 299 | 0.03 |
通過 表 9 可以看到,這里使用了超過 30 萬個不同的集合,而且僅集合本身(不考慮其中包含的數據)就占用了 206MB 的 Java 堆用量中的 42.9MB(21%)。這就意味著,如果您能更改集合類型,或者確保集合的大小更加準確,那么就有可能實現可觀的內存節約。
回頁首
通過 Memory Analyzer 查找低填充率
IBM Java 監控和診斷工具(Memory Analyzer 工具是在 IBM Support Assistant 中提供的)可以分析 Java 集合的內存使用情況(請參閱參考資料 部分)。其功能包括分析集合的填充率和大小。您可以使用這樣的分析來識別需要優化的集合。
Memory Analyzer 中的集合分析位于 Open Query Browser -> Java Collections 菜單中,如圖 14 所示:
圖 14. 在 Memory Analyzer 中分析 Java 集合的填充率
在判斷當前大小超出需要的大小的集合時,圖 14 中選擇的 Collection Fill Ratio 查詢是最有用的。您可以為該查詢指定多種選項,這些選項包括:
- 對象:您關注的對象類型(集合)
- 分段:用于分組對象的填充率范圍
將對象選項設置為 "java.util.Hashtable"、將分段選項設置為 "10",之后運行查詢將得到如圖 15 所示的輸出結果:
圖 15. 在 Memory Analyzer 中對 Hashtable 的填充率分析
圖 15 表明,在java.util.Hashtable 的 262,234 個實例中,有 127,016 (48.4%) 的實例完全未空,幾乎所有實例都僅包含少量條目。
隨后便可識別這些集合,方法是選擇結果表中的一行,右鍵單擊并選擇 list objects -> with incoming references,查看哪些對象擁有這些集合,或者選擇list objects -> with outgoing references,查看這些集合中包含哪些條目。圖 16 展示了查看對于空Hashtable 的傳入引用的結果,圖中展開了一些條目:
圖 16. 在 Memory Analyzer 中對于空 Hashtable 的傳入引用的分析
圖 16 表明,某些空 Hashtable 歸 javax.management.remote.rmi.NoCallStackClassLoader 代碼所有。
通過查看 Memory Analyzer 左側面板中的 Attributes 視圖,您就可以看到有關Hashtable 本身的具體細節,如圖 17 所示:
圖 17. 在 Memory Analyzer 中檢查空 Hashtable
圖 17 表明,Hashtable 的大小為 11(默認大小),而且完全是空的。
對于 javax.management.remote.rmi.NoCallStackClassLoader 代碼,可以通過以下方法來優化集合使用:
- 延遲分配 Hashtable:如果Hashtable 為空是經常發生的普遍現象,那么僅在存在需要存儲的數據時分配Hashtable 應該是一種合理的做法。
- 將 Hashtable 分配為準確的大小:由于使用默認大小,因此完全可以使用更為準確的初始大小。
這些優化是否適用取決于代碼的常用方式以及通常存儲的是哪些數據。
PlantsByWebSphere 示例中的空集合
表 10 展示了分析 PlantsByWebSphere 示例中的集合來確定哪些集合為空時的分析結果:
表 10. WebSphere Application Server v7 中 PlantsByWebSphere 的空集合使用量
| Hashtable | 262,234 | 127,016 | 48.4 |
| WeakHashMap | 19,562 | 19,465 | 99.5 |
| HashMap | 10,600 | 7,599 | 71.7 |
| ArrayList | 9,530 | 4,588 | 48.1 |
| HashSet | 1,551 | 866 | 55.8 |
| Vector | 1,271 | 622 | 48.9 |
表 10 表明,平均而言,超過 50% 的集合為空,也就是說通過優化集合使用能夠實現可觀的內存占用節約。這種優化可以應用于應用程序的各個級別:應用于 PlantsByWebSphere 示例代碼中、應用于 WebSphere Application Server 中,以及應用于 Java 集合類本身。
在 WebSphere Application Server 版本 7 與版本 8 之間,我們做出了一些努力來改進 Java 集合和中間件層的內存效率。舉例來說,java.util.WeahHashMap 實例的開銷中,有很大一部分比例源于其中包含用來處理弱引用的java.lang.ref.ReferenceQueue 實例。圖 18 展示了 32 位 Java 運行時中的一個WeakHashMap 的內存布局:
圖 18. 32 位 Java 運行時中的一個 WeakHashMap 的內存布局
圖 18 表明,ReferenceQueue 對象負責保留占用 560 字節的數據,即便在 WeakHashMap 為空、不需要ReferenceQueue 的情況下也是如此。對于 PlantsByWebSphere 示例來說,在空WeakHashMap 的數量為 19,465 的情況下,ReferenceQueue 對象將額外增加 10.9MB 的非必要數據。在 WebSphere Application Server 版本 8 和 IBM Java 運行時的 Java 7 發布版中,WeakHashMap 得到了一定的優化:它包含一個 ReferenceQueue,這又包含一個Reference 對象數組。該數組已經更改為延遲分配,也就是說,僅在向ReferenceQueue 添加了對象的情況下執行分配。
回頁首
結束語
在任何給定應用程序中,都存在著數量龐大(或許達到驚人的程度)的集合,復雜應用程序中的集合數量可能會更多。使用大量集合往往能夠提供通過選擇正確的集合、正確地調整其大小(或許還能通過延遲分配集合)來實現有時極其可觀的內存占用節約的范圍。這些決策最好在設計和開發的過程中制定,但您也可以利用 Memory Analyzer 工具來分析現有應用程序中存在內存占用優化潛力的部分。
總結
以上是生活随笔為你收集整理的从Java代码到Java堆理解和优化您的应用程序的内存使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入分析 Java I/O 的工作机
- 下一篇: Java内存分配原理