天天看點

Java的記憶體布局 從 Java 代碼到 Java 堆

from: https://www.ibm.com/developerworks/cn/java/j-codetoheap/

從 Java 代碼到 Java 堆

了解和優化您的應用程式的記憶體使用

Chris Bailey

, Java 支援架構師, IBM

簡介: 本文将為您提供 Java™ 代碼記憶體使用情況的深入見解,包括将 

int

 值置入一個

Integer

 對象的記憶體開銷、對象委托的成本和不同集合類型的記憶體效率。您将了解到如何确定應用程式中的哪些位置效率低下,以及如何選擇正确的集合來改進您的代碼。

本文的标簽:  

api

java java優化 java集合 jvm jvm_(java_virtual_machine) j對象記憶體

,

metys 從java代碼到java堆

... 

更多标簽 代理 代碼到 記憶體 應用開發 性能 理論 算法 編碼 編譯器 标記本文!

釋出日期: 2012 年 3 月 29 日 

級别: 中級 

原創語言: 

英文

通路情況 : 8573 次浏覽 

評論: 4 (

檢視

 | 

添加評論

 -

登入)

Java的記憶體布局 從 Java 代碼到 Java 堆

 平均分

(33個評分)

為本文評分

優化應用程式代碼的記憶體使用并不是一個新主題,但是人們通常并沒有很好地了解這個主題。本文将簡要介紹 Java 程序的記憶體使用,随後深入探讨您編寫的 Java 代碼的記憶體使用。最後,本文将展示提高代碼記憶體效率的方法,特别強調了 

HashMap

 和 

ArrayList

等 Java 集合的使用。

背景資訊:Java 程序的記憶體使用

參考知識

如需進一步了解 Java 應用程式的程序記憶體使用,請參閱 Andrew Hall 撰寫的 developerWorks 文章 “記憶體詳解”。這篇文章介紹了 

記憶體詳解

 以及 

AIX®

 提供的布局和使用者空間,以及

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(百億億位元組)。

處理器架構提供的部分可尋址範圍由 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 程序。在 

 一節中提到,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 運作時的對象中的字段大小
字段類型 字段大小(位)
對象 數組
32 位 64 位

boolean

32 8

byte

char

16

short

int

float

long

64

double

對象字段 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

 之外,此清單是按功能和記憶體開銷進行降序排列的。(

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

 開銷
一個 10K 集合的開銷

HashMap

搜尋/插入/删除性能 O(1):所用時間是一個常量時間,無論要素數量如何都是如此(假設無散列沖突)

HashMap

HashMap

Map

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

128 個位元組
64 位元組加上每個條目 36 位元組
~360K

Hashtable

Hashtable

 與 

HashMap

 相似,也是 

Map

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 位元組

如您所見,

Hashtable

 的預設容量比 

HashMap

 要稍微小一些(分别是 11 與 16)。除此之外,兩者之間的主要差别在于 

Hashtable

 無法接受空鍵和空值,而且是預設同步的,但這可能是不必要的,還有可能降低集合的性能。

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):所用時間與元素數量線性相關。

ArrayList

ArrayList

List

 接口的可變長數組實作。Java Platform SE 6 API 文檔對于 

ArrayList

不同于 

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

StringBuffer

16 的字元數組,在 

StringBuffer

 為空時,資料總大小為 72 位元組。

與集合相似,

StringBuffer

 擁有預設容量和重新調整大小的機制。表 7 顯示了 

StringBuffer

表 7. 一個 

StringBuffer

72 個位元組
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. 集合屬性彙總
集合 10K 條目的開銷 準确設定大小? 擴充算法

HashSet

O(1) 144 360K x2

HashMap

128

Hashtable

11 104 x2+1

LinkedList

O(n) 1 48 240K

ArrayList

88 40K x1.5

StringBuffer

72 24

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 的集合使用量
集合類型 執行個體數量 集合總開銷 (MB)

Hashtable

262,234 26.5

WeakHashMap

19,562 12.6

HashMap

10,600 2.3

ArrayList

9,530 0.3

HashSet

1,551

Vector

1,271 0.04

LinkedList

1,148 0.1

TreeMap

299 0.03
總計 306,195 42.9

通過 

表 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

127,016 48.4

WeakHashMap

19,465 99.5

HashMap

7,599 71.7

ArrayList

4,588 48.1

HashSet

866 55.8

Vector

622 48.9
304,748 160,156 52.6
表 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 工具來分析現有應用程式中存在記憶體占用優化潛力的部分。

學習

獲得産品和技術

  • :Memory Analyzer 将 Eclipse Memory Analyzer Tool (MAT) 的診斷功能引入了 IBM Java 虛拟機。
  • IBM Extensions for Memory Analyzer :IBM Extensions for Memory Analyzer 提供了調試一般 Java 應用程式的附加功能,還提供了調試特定 IBM 軟體産品的功能。
  • Eclipse Memory Analyzer Tool (MAT) :MAT 有助于查找記憶體洩漏,識别高記憶體占用問題。
  • 按照最适合您的方式  IBM 産品評估試用版軟體 :下載下傳産品試用版、線上試用産品、在雲環境中使用産品,或者抽出幾個小時的時間通過 

    IBM

    SOA 人員沙箱

     了解如何有效地實作面向服務架構。

讨論

  • IBM 的 Java 應用程式故障診斷 :閱讀 Chris Bailey 和他的同僚撰寫的這篇部落格文章,獲得有關 IBM 提供的 Java 應用程式故障診斷工具的新聞和資訊。 
  • 加入  developerWorks 中文社群 。與其他 developerWorks 使用者聯系,浏覽開發人員推動的部落格、論壇、小組和 wiki。