2011-10-14 64 views
3

我想對java中實現的緩存進行相當準確的度量。請告訴我,如果這種方法是可能的。簡單數據結構的Java內存使用

我有一個散列映射到一個字符串數組的字符串。有什麼方法可以獲得這種數據結構的良好映射嗎?

  1. 如何獲取字符串的大小?調用String.toByte()並添加一些加持有對象的開銷?

  2. 是一個字符串數組的所有字符串的總和?還是有一些開銷?

  3. hashmap是否也有一些overead,也許包裝對象到一些入口對象?

  4. 對於地圖中所有未使用的空間,散列表仍然分配一些空間,我可以總結2 * null pointer爲地圖中的所有未使用的空間嗎?

我很高興與部分的答案也poiting我在正確的方向。

+0

(至少在Sun JVM)字符串是內部持有作爲char []而不是[]一個字節,所以一個字符串的堆的大小近似爲2 * str.length()加一個位用於其他領域和對象開銷。 –

回答

2

的實際內存開銷對象實例隱含依賴於JVM實現內部的一些細節,並且可能難以定義,因爲它可以在整個對象的生命週期變化(在垃圾收集器中,對象可以在使用不同內存管理結構的世代之間「移動」)。

一個非常粗略的近似值是,任何對象的每個實例都包含兩個「字」(32位機器上的兩個32位值,64位機器上的兩個64位值)。其中的一個或多或少是指向該對象的實例的指針,另一個則保存某個對象狀態,例如該對象的監視器(您使用​​鎖定的那個對象)。然後是對象字段。對於一個數組,數組的長度必須寫在對象的某個地方,以及值。

此時,請查看Java類的源代碼(查找JDK發行版中名爲src.zip的文件)。在String.java文件中,我們可以看到,在內部,String實例有四個字段:對值爲char的數組的引用,以及三個int(其中一個是數組中第一個字符串的索引,第二個是字符串長度,第三個緩存字符串散列碼)。因此,對於一個32位機,可以估算出,的Ñ字符String實例的最小的內存使用情況是的總和:

  • String實例對象頭兩個32位字
  • 四爲String實例字段的32位字
  • 用於陣列實例頭和長度3個32位字
  • ñ爲字符本身16位字(一個char是16位)

這只是一個最低限度,因爲String實例只引用內部字符數組的,所以數組的內存大小可以更大。另一方面,字符陣列可以在多個String實例之間共享。這種結構允許String.substring()非常快:新String實例內部使用相同的陣列,所以沒有涉及數據複製;但這也意味着如果你有一個很大的字符串,拿一個小的字符串,並且存儲小的子字符串,你實際上也保留在RAM中的大陣列(對於String實例str,你可以使new String(str)得到一個新的實例,它將在內部使用一個新分配和修剪過的數組實例)。好的一面,如果你有兩個字符串,一個是另一個的子字符串,並且你將兩個字符串存儲在緩存中,那麼你只需爲公共內部數組支付一次。

因此,即使不考慮GC暗示的所有隱藏成本,也很難知道「字符串的內存大小」是什麼意思:如果兩個String實例共享相同的內部數組,那麼如何計算「大小「的每個字符串?

縱觀源HashMap會告訴你,有這也是分配的內部情況;每個存儲值有一個HashMap.Entry實例的數組和一個HashMap.Entry實例。陣列大小根據條目數量和配置的負載因數進行動態調整。

由於佔內存大小是很難的,一個完全不同的解決方法是讓當舊的緩存項應該被刪除的GC自行決定。這在內部使用「軟引用」:它們是某些類型的指針,當內存變得緊張時,GC可能將其設置爲null(破壞引用可能允許GC釋放更多對象)。這使得粗略的「內存感知」緩存根據可用的RAM自動修剪。一個有用的圖書館是谷歌的番石榴和它的MapMaker類。

+0

感謝您的好解釋。一個關於軟引用的問題:我聽到很多人說他們不適合緩存,因爲他們放棄了信息太快而且無法控制行爲。你有什麼選擇? –

+0

我想我不能使用MapMaker,因爲它使用==而不是等於。我不能保證用作鍵的字符串是==用於再次獲取對象的字符串。我只知道他們是平等的 –

+1

@Franz:由於你描述的原因,軟參考語言實際上是無用的。現在(Java 6及以上版本)它們相當不錯。如果您正在使用服務器虛擬機,則只有在內存變得稀缺的情況下才能清除它們。 –

3

我認爲一個很好的實踐方法是使用內存分析器,如YourKit

+0

可能是一個很好的方法只是不在我的預算:-) –

3

你試過Instrumentation.getObjectSize()?這可能告訴你想要你想要的東西,但JavaDoc聲稱它只是一個估計值。

+0

如果我直接在地圖上調用它,它是否給我一個淺的大小或全尺寸,包括所有插入的對象?如果它包含所有內容,它將遍歷完整的結構,這可能會導致可怕的運行時間? –

+1

正如JDK JavaDoc的典型代碼,它不會告訴你這樣的重要事情。你可以嘗試通過比較你從一個空的'Map'和一個'Map'中獲得的值以及其中的幾千個元素來測試。 – uckelman

1

1)讓我們假設,儘管它不能保證(不同JVM可以採取不同的)

2)串的總和加上保持物體(陣列)

3)的開銷當然,一許多。對象被包裝成條目,然後將這些條目存儲到內部HashSet中,等等......至少在Oracle JVM中。

4)地圖中沒有「未使用」的空間......你是什麼意思?

那麼總之,不幸的是,沒有辦法得到這些問題的準確答案。它取決於虛擬機,GC,操作系統等...一個分析器可以給你一些有關一種配置的有用信息,但這是你可能希望得到的最多的信息。

它是通過設計:Java及其垃圾收集器希望您不必擔心內存分配和管理細節。大部分時間都很棒,在你的情況下這是一個負擔。無論如何,你爲什麼有這樣的需求?

+0

謝謝,如果一個哈希映射達到一定的大小其內部結構增長,散列函數改變等我猜想它在內部使用數組。如果內部數組爲103,並且只插入了40個對象,則我有63個空格。瞭解我在說什麼? –

+0

數組的增長是由負載因子引導的,默認爲0.75。這意味着當地圖爲75%「滿」時,它將被調整大小(加倍)。默認大小是16.這兩個參數都可以在構造函數中設置。如果您知道您的元素數量並希望確保最低的開銷(以犧牲性能爲代價),您甚至可以使用固定的大小和負載因子1 – solendil

+0

我想指定與XMX相關的高速緩存大小。使用寬鬆指針在緩存時不是一個好主意,因爲它可以快速下降到很多東西。在代碼中隨處捕捉OOME,然後丟棄一些對象也不是很聰明。估計總內存消耗量不準確,需要首先調用gc(),這會破壞運行時。似乎沒有任何剩餘的選項... :-( –

0

一個簡單的方法來量化你的內存使用量是使用下列內容: jmap -histo:live <pid>(你的java進程的進程ID)

這會給你堆的直方圖。對於每個Java類,都會打印對象數量,以字節爲單位的內存大小以及完全限定的類名稱。
您還可以: jmap -dump:live pid
以hprof二進制格式轉儲Java堆。
我會看更多jmap。當你的瓶頸是java的內存時,它是非常有用的。
例如,您可以創建一個腳本,做一個JMAP每30秒-histo。然後,您可以繪製輸出圖並查看您的java類中創建的每個對象的內存變化。

這裏是JMAP -histo的一個例子:

$ jmap -histo `pgrep java` |more 
num #instances #bytes class name 
-------------------------------------- 
    1: 224437 27673848 [C 
    2:  38611 23115312 [B 
    3:  47801 12187536 [I 
    4: 208624  8344960 java.lang.String 
    5:  45332  6192904 <constMethodKlass> 
    6:  45332  5450864 <methodKlass> 
    7:  3889  4615536 <constantPoolKlass> 
    8:  45671  4193136 [Ljava.lang.Object; 
    9:  66203  3222312 <symbolKlass> 
10:  3889  3192264 <instanceKlassKlass> 
11:  3455  2999296 <constantPoolCacheKlass> 
12:  19754  1106224 java.nio.HeapCharBuffer 

更多的例子here


此外,配置您的過程將是不錯的選擇了。
我會建議使用visualvm (free)jprofiler7 (not free, but awesome!)