什么是保留堆?
我需要多少內存? 在構建解決方案,創建數據結構或選擇算法時,您可能會問自己(或其他人)這個問題。 如果此圖包含1,000,000條邊并且我使用HashMap進行存儲,此圖是否適合我的3G堆? 我可以在構建自定義緩存解決方案時使用標準的Collections API ,還是它們造成的開銷過多?
顯然,簡單問題的答案要復雜一些。 在這篇文章中,我們將對此做一個初步的了解,看看實際上兔子洞有多深。
標題中問題的答案分為幾個部分。 首先,我們需要了解您是對淺堆大小還是保留堆大小感興趣。
淺堆很容易–它僅由對象本身占用的堆組成。 如何計算它有些細微差別,但對于本文的范圍,我們將其保留不變。 請繼續關注有關同一主題的未來帖子。
保留的堆在許多方面都更加有趣。 很少有人對淺堆感興趣,在大多數情況下,您的實際問題可以轉換為“如果我從內存中刪除該對象,那么垃圾收集器現在可以釋放多少內存”。
現在,我們都記得,所有Java垃圾回收(GC)算法都遵循以下邏輯:
- GC將某些對象視為“重要”對象。 這些被稱為GC根,并且(幾乎)從不丟棄。 例如,它們是當前正在執行的方法的局部變量和輸入參數,應用程序線程,來自本機代碼的引用以及類似的“全局”對象。
- 從那些GC根目錄引用的任何對象都被假定為正在使用,因此未被GC丟棄。 一個對象可以用Java中的不同方式引用另一個對象,在最常見的情況下,對象A存儲在對象B的字段中。在這種情況下,我們說“ B引用A”。
- 重復該過程,直到訪問了可以從GC根過渡獲取的所有對象并將其標記為“使用中”為止。
- 其他所有東西都沒有使用,可以扔掉。
現在說明如何計算保留的堆,讓我們通過以下示例對象遵循上述算法:
為了簡化示例,讓我們估計所有對象O1-O4的淺堆都為1024B = 1kB。 讓我們開始計算這些對象的保留大小。
- O4沒有對其他對象的引用,因此其保留大小等于其1kB的淺大小。
- O3引用了O4。 因此,垃圾收集O3意味著O4也有資格進行垃圾收集,因此我們可以說O3保留的堆為 2kB 。
- O2引用了O3。 但是現在要注意,從O2刪除指向O3的指針并不能使O3符合GC的條件,因為O1仍然有指向它的指針。 因此, O2保留的堆只有1kB 。
- 另一方面,O1是在此小圖中保留所有引用的對象,因此,如果我們刪除O1,則該圖上的所有內容都將被垃圾回收。 因此, O1保留的堆為4kB 。
實際上有什么影響? 實際上,了解淺堆大小和保留堆大小之間的差異使使用內存分析器和堆轉儲分析器之類的工具成為可能–例如,如果您不知道如何區分這兩種方法,那么挖掘Eclipse MAT可能被證明是不可能的。堆大小測量的類型。
什么是淺堆?
本文是本系列的第二篇文章,我們將嘗試回答這些問題。 最后一篇文章解釋了對象的保留大小和淺大小之間的區別。 在本文中,我們還提供了一個如何計算數據結構的保留堆大小的示例。 在今天的文章中,我們將擴展上一篇文章中所謂的“簡單”。 即– 什么是以及如何測量對象使用的淺堆。
在第一篇文章中,我們指出了計算淺堆大小很容易–從而僅由對象本身占用的堆組成,從而大大降低了復雜性。 但是,如何計算對象“自身”需要多少內存呢? 顯然有一個公式:
Shallow Heap Size = [reference to the class definition] + space for superclass fields + space for instance fields + [alignment]
似乎不太有用,是嗎? 讓我們嘗試使用以下示例代碼來應用公式:
class X {int a;byte b;java.lang.Integer c = new java.lang.Integer();
}
class Y extends X {java.util.List d;java.util.Date e;
}
現在,我們努力回答的問題是– Y實例需要多少淺堆大小? 讓我們開始計算它,假設我們使用的是32位x86體系結構:
作為起點– Y是X的子類,因此它的大小包括超類中的“某物”。 因此,在計算Y的大小之前,我們先考慮計算X的淺大小。
跳到X的計算中,前8個字節用于引用其類定義。 該引用始終存在于所有Java對象中,并且被JVM用來定義以下狀態的內存布局。 它還具有三個實例變量–一個int,一個Integer和一個字節。 這些實例變量需要堆,如下所示:
- 一個字節就是應該的。 內存中有1個字節。
- 我們的32位架構中的int需要4個字節。
- 對整數的引用也需要4個字節。 請注意,在計算保留堆時,我們還應考慮包裝到Integer對象中的原語的大小,但是在此處計算淺堆時,我們在計算中僅使用4個字節的參考大小。
那么-是嗎? X的淺堆=從引用到類定義的8個字節+ 1個字節(該字節)+ 4個字節(int)+ 4個字節(對Integer的引用)= 17個字節? 實際上–不 。 現在起作用的是對齊(也稱為填充)。 這意味著JVM以8字節的倍數分配內存,因此如果我們創建X的實例,我們將分配24字節而不是17字節。
如果您可以跟隨我們到這里,那很好,但是現在我們嘗試使事情變得更加復雜。 我們不是在創建X的實例,而是在創建Y的實例。這意味著–我們可以從引用中減去8個字節,以引用類定義和對齊方式。 乍一看可能不太明顯,但是–您是否注意到,在計算X的淺大小時,我們沒有考慮到它也擴展了java.lang.Object,因為即使您未在其中明確聲明它,所有類也會這樣做。您的源代碼? 我們不必考慮超類的標頭大小,因為JVM足夠聰明,可以從類定義本身進行檢查,而不必一直將其復制到對象標頭中。
對齊方式也一樣–創建對象時,您只能對齊一次,而不能在超類/子類定義的邊界對齊。 因此,可以肯定地說,當創建X的子類時,您只會從實例變量繼承9個字節。
最后,我們可以跳到初始任務并開始計算Y的大小。正如我們所看到的,我們已經在超類字段中丟失了9個字節。 讓我們看看實際構造Y實例時將添加什么。
- Y的標頭引用其類定義占用8個字節。 與以前的相同。
- 日期是對對象的引用。 4字節。 簡單。
- 該列表是對集合的引用。 同樣是4個字節。 不重要的。
因此,除了超類中的9個字節之外,我們還有8個字節的頭空間,2×4個字節的數據來自兩個引用(列表和日期)。 Y實例的總淺層大小為25個字節,對齊為32個字節。
為了使計算更容易遵循,我們將其匯總在下圖中:
1個 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18歲 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | |
對齊 | 對齊 | 對齊 | 對齊 | |||||||||||||||||||||||||||||
X | 賓語 | 一個 | b | C | ||||||||||||||||||||||||||||
? | 賓語 | 一個 | b | C | d | ? |
您可以用這些知識做什么? 加上計算保留堆大小的技巧(在我的最新文章中已經介紹過 ),您現在擁有了計算數據結構實際需要多少內存的最終能力。
為了使事情變得更加有趣,我們創建了一個實用程序,該實用程序可以測量對象的淺堆和保留堆的大小。 在不久的將來,我們將免費提供該工具。 訂閱我們的Twitter feed,敬請關注!
測量,不要猜測
看起來很簡單的任務實際上可能變得有些復雜。 在計算對象的內存占用量時,您需要牢記很多不同方面:
- 我需要測量淺堆還是保留堆大小?
- 我是否要針對32位或64位架構進行計算?
- 我是在x86,SPARC,POWER還是其他無法想象的東西上運行?
- 是否使用壓縮或未壓縮的普通對象指針?
- [在此處輸入您擔心或不完全理解的其他內容]
當試圖滿足另一個截止日期時,在嘗試估計數據結構的大小時牢記所有這些方面是完全不合理的。 因此,我們繼續將Java Champion Heinz Kabutz發布的代碼打包為Java代理,并提供了一種輕松的方法將其添加到您的應用程序中。
添加代理使您可以輕松地跟蹤實際環境中數據結構占用的內存量。 并做到了沒有其他選擇所帶來的復雜性。 在下面的四個簡單步驟中,您正在運行并最終了解寶貴的緩存實際消耗了多少內存:
步驟1: 下載代理。 不用擔心,它只有幾千字節。
步驟2:解壓縮下載的代理。 您會看到它與源代碼以及如何使用它的樣例一起打包在一起。 隨意使用代碼。
nikita-mb:sizeof nikita$ ls -l
total 16
-rw-r--r-- 1 nikita staff 1696 Aug 28 22:12 build.xml
-rw-r--r-- 1 nikita staff 3938 Aug 28 22:33 sizeofagent.jar
drwxr-xr-x 5 nikita staff ?170 Aug 28 10:44 src
步驟3:嘗試捆綁的測試用例。 捆綁的測試用例測量的數據結構與我們在博客文章中描述的有關淺堆大小測量的數據結構相同。 對于那些不愿意來回點擊的人,這里再次是代碼:
class X {int a;byte b;java.lang.Integer c = new java.lang.Integer();
}
class Y extends X {java.util.List d;java.util.Date e;
}
該測試用例隨Ant測試一起提供,以編譯和運行示例。 如果您使用的是32位體系結構,請運行ant test
或ant test-32
。 使用ant test
運行所有測試時,應該看到以下輸出:
nikita-mb:sizeof nikita$ ant testBuildfile: /Users/nikita/workspace/sizeof/build.xmlinit:compile:test32:[java] java.lang.Object: shallow size=8 bytes, retained=8 bytes[java] eu.plumbr.sizeof.test.X: shallow size=24 bytes, retained=40 bytes[java] eu.plumbr.sizeof.test.Y: shallow size=32 bytes, retained=48 bytestest64+UseCompressedOops:[java] java.lang.Object: shallow size=16 bytes, retained=16 bytes[java] eu.plumbr.sizeof.test.X: shallow size=24 bytes, retained=40 bytes[java] eu.plumbr.sizeof.test.Y: shallow size=32 bytes, retained=48 bytestest64-UseCompressedOops:[java] java.lang.Object: shallow size=16 bytes, retained=16 bytes[java] eu.plumbr.sizeof.test.X: shallow size=32 bytes, retained=56 bytes[java] eu.plumbr.sizeof.test.Y: shallow size=48 bytes, retained=72 bytestest:BUILD SUCCESSFUL
Total time: 2 seconds
從上面的測試中,您可以看到例如在32位體系結構上,Y的淺堆消耗32個字節,保留堆占用48個字節。 在帶有-XX:-UseCompressedOops
的64位體系結構上,淺層大小增加到48個字節,保留堆大小增加到72個字節。 如果您對我們如何計算這些數字感到困惑,那么請從本系列的先前文章中了解什么是以及如何計算淺層和保留堆大小。
步驟4:將代理附加到您自己的Java應用程序。 為此,將-javaagent:path-to/sizeofagent.jar
到JVM啟動腳本中。 現在,您可以測量通過調用淺堆消耗MemoryCounterAgent.sizeOf(yourObject)
或測量通過調用保留堆消耗MemoryCounterAgent.deepSizeOf(yourObject)
直接在你的代碼。 另請參閱捆綁的ant腳本和eu.plumbr.sizeof.test.SizeOfSample
類,以防您在執行過程中感到困惑。
當然,您有許多選擇,尤其是以內存分析器和APM解決方案的形式。 但是,這個小型代理將快速完成其任務,幾乎不需要設置或學習。 好吧,至少我們玩得很開心。 而不是處理我們的產品積壓 。
PS。 在撰寫本文時,以下在線資源被用作靈感來源:
- http://memoryanalyzer.blogspot.com/2010/02/heap-dump-analysis-with-memory-analyzer.html
- http://www.javamex.com/tutorials/memory/object_memory_usage.shtml
- http://www.javamex.com/tutorials/memory/instrumentation.shtml
- http://kohlerm.blogspot.com/2008/12/how-much-memory-is-used-by-my-java.html
- http://www.javaspecialists.eu/archive/Issue142.html
并且-不要忘了將此代碼發送給Heinz Kabutz,他在2007年3月的Java專家通訊中首次發布了該代碼。
參考: 我需要多少內存(第1部分)–什么是保留堆? , 我需要多少內存(第2部分)–什么是淺堆? , 我需要多少內存(第3部分)–測量,請不要從Plumbr Blog博客的JCG合作伙伴 Nikita Salnikov Tarnovski那里猜到 。
翻譯自: https://www.javacodegeeks.com/2012/12/how-much-memory-do-i-need.html