為什么JVM指定-Xmx參數后占用內存會變少?
“嘿,你能順便過來看看這個奇怪的事情嗎?” 就是讓我提供支持的這個事情,驅使我寫下這篇博客的。這個特殊的問題是,不同工具給出的可用內存的報告是不一樣的。
簡而言之,工程師正在調查特定應用程序的內存使用。根據以往的經驗,他給這個應用指定了2G堆內存。但是不知道什么原因,JVM工具似乎不能確定這個程序到底有多少內存。例如 jconsole 探測可用堆總共為1963M,但 jvisualvm 報告稱堆為2048M。到底哪一個是正確的呢?為什么另一個給出了不一樣的信息呢?
這的確很不可思議,特別是以往的認知被突然改變。表面上JVM沒有耍任何花招:
- -Xmx 和 -Xms 是相等的,這就使得報告的數字不會隨著堆實時增加。
- JVM避免通過內存的自適應策略(-XX:-UseAdaptiveSizePolicy)動態改變內存池的大小。
重現不同
搞懂這個問題的第一步是深入這些工具的實現方式。一般通過標準API查看可用內存會像下面這樣:
System.out.println("Runtime.getRuntime().maxMemory()="+Runtime.getRuntime().maxMemory()); 的確,這好像是工具首先會被用到的方式。尋找答案的第一步是找出可復現的測試用例。為了這個目的,我寫了下面這段代碼:
package eu.plumbr.test; //imports skipped for brevitypublic class HeapSizeDifferences {
static Collection<Object> objects = new ArrayList<Object>(); static long lastMaxMemory = 0;
public static void main(String[] args) { try { List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); System.out.println("Running with: " + inputArguments); while (true) { printMaxMemory(); consumeSpace(); } } catch (OutOfMemoryError e) { freeSpace(); printMaxMemory(); } }
static void printMaxMemory() { long currentMaxMemory = Runtime.getRuntime().maxMemory(); if (currentMaxMemory != lastMaxMemory) { lastMaxMemory = currentMaxMemory; System.out.format("Runtime.getRuntime().maxMemory(): %,dK.%n", currentMaxMemory / 1024); } }
static void consumeSpace() { objects.add(new int[1_000_000]); }
static void freeSpace() { objects.clear(); } }</pre>
這段代碼通過在一個 new int[1000000] 的循環中分配內存塊,檢測當前在實時JVM中的可用內存。無論何時,只要最后知道的內存大小改變時,都會通過打印出 ofRuntime.getRuntime().maxMemory()__ 報告出來,類似于如下這樣:
Running with: [-Xms2048M, -Xmx2048M] Runtime.getRuntime().maxMemory(): 2,010,112K.結果確實如此——有時甚至指定JVM有2G可用堆,但是運行著莫名其妙地發現其中的85M找不到了。你可以通過運用 2,010,112K 除以 1024 轉化Runtime.getRuntime().maxMemory() 的輸出到MB來復查我的計算。實際結果等于1963M,比起實際的 2048M 少了 85M。
尋求根本原因
重現這個現象之后,我做了如下的筆記——采用不同的GC算法運行似乎也產生不同的結果:
| GC algorithm | Runtime.getRuntime().maxMemory() |
| -XX:+UseSerialGC | 2,027,264K |
| -XX:+UseParallelGC | 2,010,112K |
| -XX:+UseConcMarkSweepGC | 2,063,104K |
| -XX:+UseG1GC | 2,097,152K |
除了G1消費了我實際給的2G之外,任何其它GC算法似乎始終會半隨機地丟失一部分內存。
現在是時候剖析一下JVM的源代碼了,在 CollectedHeap 的源代碼中,我發現了下面這些:
//對java.lang.Runtime.maxMemory()的支持: //返回虛擬機提供給“標準”java對象的最大內存。 //這個基于保留的地址空間,但是不應該包括虛擬機使用內部統計或臨時存儲的這部分空間。 //(例如:在青年代中,殘留空間之一) virtual size_t max_capacity() const = 0;
不得不承認答案隱藏得很深。但真相還是在好奇心的驅使下找到——事實上,某些情況下殘留空間其中一些可能被排除在內存計算之外。
從這里開始就一帆風順了。打開GC日志發現,確實在設置2G內存時,Parallel和CMS算法都會在不同程度上,設置殘留的空間是可變的。例如,以Parallel算法為例GC的日志演示如下所示:
Running with: [-Xms2g, -Xmx2g, -XX:+UseParallelGC, -XX:+PrintGCDetails] Runtime.getRuntime().maxMemory(): 2,010,112K.... rest of the GC log skipped for brevity ...
PSYoungGen total 611840K, used 524800K [0x0000000795580000, 0x00000007c0000000, 0x00000007c0000000) eden space 524800K, 100% used [0x0000000795580000,0x00000007b5600000,0x00000007b5600000) from space 87040K, 0% used [0x00000007bab00000,0x00000007bab00000,0x00000007c0000000) to space 87040K, 0% used [0x00000007b5600000,0x00000007b5600000,0x00000007bab00000) ParOldGen total 1398272K, used 1394966K [0x0000000740000000, 0x0000000795580000, 0x0000000795580000)</pre>
從上面你可以看到,Eden空間被設置為了524800K,殘留空間都被設為了 87040K,Old空間大小為 1398272K。把Eden、Old和殘留空間之一加在一起等于2010112K,確認丟失的 85 或 87040K 確實是保留的殘留空間。
總結
讀完這篇文章后,相信你現在已經準備好以一種新的視角深入到Java API的實現細節。下次遇到可視化工具的總可用堆大小略低于Xmx規定的大小時,你就知道少的那部分等于你一個殘留空間的大小。
不得不承認的一個事實是,在日常的編程中不是特別有用,但是這不是我寫這篇文章的初衷。相反地,寫這篇文章目的是為了強調我在優秀工程師身上看到的特質——好奇心。優秀的工程師總是想去知道,那些東西的工作方式并探究為什么它們會像那樣工作。有時候答案藏匿地很深,但仍然建議你去試圖尋求答案。最終,在這個過程中獲取的知識,將會讓你受益無窮。
原文鏈接: javacodegeeks 翻譯: ImportNew.com - 張 健
譯文鏈接: http://www.importnew.com/15934.html