為什么JVM指定-Xmx參數后占用內存會變少?

gww3 10年前發布 | 12K 次閱讀 JVM Java開發

“嘿,你能順便過來看看這個奇怪的事情嗎?” 就是讓我提供支持的這個事情,驅使我寫下這篇博客的。這個特殊的問題是,不同工具給出的可用內存的報告是不一樣的。

簡而言之,工程師正在調查特定應用程序的內存使用。根據以往的經驗,他給這個應用指定了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 brevity

public 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

 本文由用戶 gww3 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!