我這樣減少了26.5M Java內存

首先,請注意,JDK集合的內部並不是魔術。它們是用Java編寫的。他們的源代碼隨JDK一起提供,因此您可以在IDE中打開它。也可以在網上輕鬆找到。而且,事實證明,在優化內存佔用方面,大多數集合都不是很完善。

例如,考慮以下最簡單和最受歡迎的集合類之一:java.util.ArrayList。內部每個ArrayList維護一個Object[] elementData數組。這就是列表元素的存儲位置。讓我們看看如何管理這個數組。

當您ArrayList使用默認構造函數創建一個時,即invoke new ArrayList(), elementData 被設置為指向單例共享的零大小數組(elementData也可以設置為null,但是單例數組提供了一些次要的實現優勢)。將第一個元素添加到列表後,將elementData 創建一個真實的唯一數組,並將所提供的對象插入其中。為避免每次添加新元素時都調整數組大小,將以長度10(“默認容量”)創建該元素。這裡有一個陷阱:如果您從不向此添加更多元素ArrayList,則elementData 陣列中10個插槽中的9個將保持為空。即使以後清除此列表,內部陣列也不會縮小。下圖總結了這個生命週期:

我這樣減少了26.5M Java內存

JDK集合是列表和地圖的標準庫實現。如果你看看一個典型的大的Java應用程序的內存快照,你會看到成千上萬甚至數百萬的實例java.util.ArrayList,java.util.HashMap等集合是在內存中的數據存儲和操作是必不可少的。但是,您是否考慮過應用程序中的所有集合是否都以最佳方式使用內存?換句話說:如果您的Java應用程序因臭名昭著的OutOfMemoryErrorGC 崩潰或經歷了長時間的GC暫停而崩潰了,您是否檢查了其收集的內存浪費?如果您的回答是“否”或“不確定”,請繼續閱讀。

這裡浪費了多少內存?絕對而言,它的計算方式是(對象指針大小)*9。如果使用HotSpot JVM(Oracle JDK隨附),則指針大小取決於最大堆大小(請參閱https://blog.codecentric.de/ zh-CN / 2014/02 / 35gb-heap-less-32gb-java-jvm-memory-oddities / )。通常,如果您指定-Xmx小於32 GB,則指針大小為4個字節;對於較大的堆,它是8個字節。因此,ArrayList使用默認構造函數進行初始化(僅添加一個元素)會浪費36或72個字節。

實際上,一個空也 ArrayList浪費了內存,因為它不承擔任何工作量,但是ArrayList對象本身的大小非零且比您想象的要大。這是因為,一方面,由HotSpot JVM管理的每個對象都有一個12字節或16字節的標頭,JVM將該標頭用於內部目的。接下來,大多數集合對象都包含該size字段,指向內部數組或另一個“工作負載載體”對象的指針,modCount用於跟蹤內容修改的字段等。因此,即使是表示空集合的最小對象也可能至少需要32個對象。內存字節。有些,例如ConcurrentHashMap,需要更多。

考慮另一個無處不在的集合類:java.util.HashMap。其生命週期與相似, ArrayList總結如下:

我這樣減少了26.5M Java內存

如您所見,HashMap 僅包含一個鍵/值對的浪費15個內部數組插槽,這將轉換為60或120個字節。這些數字很小,但是重要的是,以您的應用程序相對而言,所有集合都會損失多少內存。事實證明,某些應用程序會以這種方式浪費很多時間。例如,作者分析了幾個流行的開源Hadoop組件,在某些情況下損失了大約20%的堆!對於由經驗不足的工程師開發且未定期檢查其性能的產品,內存浪費可能更高。有足夠的用例,例如,一棵大樹中90%的節點僅包含一個或兩個子代(或根本不包含任何子代),以及其他情況下堆中充滿了0、1、2元素集合。

如果您在應用中發現未使用或未充分利用的集合,該如何解決?以下是一些常見的食譜。在這裡,我們有問題的集合被假定為數據字段的ArrayList引用Foo.list。

如果list 從未使用過大多數實例 ,請考慮將其延遲初始化。因此,以前看起來像的代碼...

<code>void addToList(Object x) {    list.add(x);}/<code>

...應重構為類似

<code>void addToList(Object x) {  getOrCreateList().add(x);}private list getOrCreateList() {    // To conserve memory, we don't create the list until the first use    if (list == null) list = new ArrayList();    return list;}/<code>

請記住,有時您需要採取其他措施來解決可能的比賽。例如,如果您維護一個ConcurrentHashMap可以同時由多個線程更新的,則延遲對其進行初始化的代碼不應允許兩個線程意外地創建此映射的兩個副本:

<code>private Map getOrCreateMap() {    if (map == null) {        // Make sure we aren't outpaced by another thread        synchronized (this) {            if (map == null) map = new ConcurrentHashMap();        }    }    return map;}/<code>

如果list或map的大多數實例僅包含少量元素,請考慮使用更合適的初始容量進行初始化,例如

<code>list = new ArrayList(4); // Internal array will start with length 4/<code>

如果在大多數情況下您的集合為空或僅包含一個元素(或鍵值對),則可以考慮一種極端的優化形式。僅當在給定的類中對集合進行完全管理時才有效,即其他代碼無法直接訪問它。想法是將數據字段的類型從例如 List更改為更通用的Object,以便它現在可以指向真實列表,也可以直接指向唯一的列表元素。這是一個簡短的草圖:

<code>// *** Old code ***private List list = new ArrayList<>();void addToList(Foo foo) { list.add(foo); }// *** New code ***// If list is empty, this is null. If list contains only one element,// this points directly to that element. Otherwise, it points to a// real ArrayList object.private Object listOrSingleEl;void addToList(Foo foo) {    if (listOrSingleEl == null) { // Empty list        listOrSingleEl = foo;    } else if (listOrSingleEl instanceof Foo) { // Single-element        Foo firstEl = (Foo) listOrSingleEl;        ArrayList list = new ArrayList<>();        listOrSingleEl = list;        list.add(firstEl);        list.add(foo);    } else { // Real, multiple-element list        ((ArrayList) listOrSingleEl).add(foo);    }}/<code>

顯然,這種優化使您的代碼可讀性降低,維護起來也更加困難。但是,如果您知道您將通過這種方式節省大量內存,或者擺脫了長時間的GC暫停,那可能是值得的。

這可能已經引起您的思考:我如何知道應用程序中的哪些集合浪費了內存,有多少?

簡短的答案是:如果沒有適當的工具,這很難發現。試圖猜測大型複雜應用程序中數據結構使用或浪費的內存量幾乎是行不通的。而且,在不確切知道內存在哪裡的情況下,您可能會花費大量時間追蹤錯誤的目標,而應用程序卻頑固地失敗了OutOfMemoryError。

因此,您需要使用工具檢查應用程序的堆。根據經驗,分析JVM內存(以可用信息量與工具對應用程序性能的影響來衡量)的最佳方法是獲取堆轉儲,然後脫機查看它。堆轉儲本質上是堆的完整快照。可以在任意時刻通過調用jmap實用程序來獲取它,或者可以將JVM配置為在失敗時自動生成它OutOfMemoryError。如果您使用Google進行“ JVM堆轉儲”,您將立即看到一堆文章,詳細解釋瞭如何獲得轉儲。

堆轉儲是大約JVM堆大小的二進制文件,因此只能使用特殊工具讀取和分析。有許多這樣的工具,包括開源和商業工具。最受歡迎的開源工具是Eclipse MAT;還有VisualVM和一些功能較弱且鮮為人知的工具。商業工具包括通用Java探查器:JProfiler和YourKit,以及專門為堆轉儲分析而構建的一種工具,稱為JXRay(免責聲明:作者開發了後者)。

與其他工具不同,JXRay會立即分析堆轉儲,以解決大量常見問題,例如重複的字符串和其他對象以及次優的數據結構。上述集合的問題屬於後一類。該工具將生成一個報告,其中包含所有收集的HTML格式信息。這種方法的優點是您可以隨時隨地查看分析結果,並輕鬆與他人共享。這也意味著您可以在任何計算機上運行該工具,包括數據中心中功能強大但“無頭”的計算機。

JXRay以字節為單位,並以已用堆的百分比來計算開銷(如果解決特定問題,將節省多少內存)。它將具有相同問題的同一類的集合歸為一類。

我這樣減少了26.5M Java內存

...然後將可從某個GC根目錄通過同一參考鏈訪問的有問題的集合分組,如下例所示

我這樣減少了26.5M Java內存

知道哪些引用鏈和/或單個數據字段(例如,INodeDirectory.children上面的數據)指向浪費大多數內存的集合,可以使您快速而精確地找出導致問題的代碼,然後進行必要的更改。

總之,配置欠佳的Java集合可能會浪費大量內存。在許多情況下,此問題很容易解決,但是有時,您可能需要以非平凡的方式更改代碼以取得重大改進。很難猜測需要優化哪些集合以產生最大的影響。為了避免浪費時間優化代碼的錯誤部分,您需要獲取JVM堆轉儲並使用適當的工具對其進行分析。


如果您喜歡本文,並且想了解有關Java集合的更多信息,請查看有關Java 集合的 所有東西的教程和文章的集合以及關注我們!


分享到:


相關文章: