為什麼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 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(); } }
這段程式碼通過在一個 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)
從上面你可以看到,Eden空間被設定為了524800K,殘留空間都被設為了 87040K,Old空間大小為 1398272K。把Eden、Old和殘留空間之一加在一起等於2010112K,確認丟失的 85 或 87040K 確實是保留的殘留空間。
總結
讀完這篇文章後,相信你現在已經準備好以一種新的視角深入到Java API的實現細節。下次遇到視覺化工具的總可用堆大小略低於Xmx規定的大小時,你就知道少的那部分等於你一個殘留空間的大小。
不得不承認的一個事實是,在日常的程式設計中不是特別有用,但是這不是我寫這篇文章的初衷。相反地,寫這篇文章目的是為了強調我在優秀工程師身上看到的特質——好奇心。優秀的工程師總是想去知道,那些東西的工作方式並探究為什麼它們會像那樣工作。有時候答案藏匿地很深,但仍然建議你去試圖尋求答案。最終,在這個過程中獲取的知識,將會讓你受益無窮。
相關文章
- JVM記憶體引數配置JVM記憶體
- 減少Spring Boot的JVM記憶體佔用的Docker三種配置Spring BootJVM記憶體Docker
- 從記憶體洩露、記憶體溢位和堆外記憶體,JVM優化引數配置引數記憶體洩露記憶體溢位JVM優化
- 字串池化,減少1/3記憶體佔用字串記憶體
- jvm的記憶體引數配置(skycto JEEditor)JVM記憶體
- 記憶體耗盡後Redis會發生什麼記憶體Redis
- Redis資料已經過期了,為什麼還佔用記憶體?Redis記憶體
- mysql執行緒獨佔記憶體引數MySql執行緒記憶體
- filebeat實踐-記憶體佔用-最大記憶體佔用記憶體
- 為什麼說列舉更佔記憶體,列舉原理是什麼?記憶體
- Eclipse-設定JVM的記憶體引數EclipseJVM記憶體
- JVM記憶體引數詳解及其配置調優JVM記憶體
- 谷歌Chrome瀏覽器引入省記憶體/省電模式:減少記憶體佔用谷歌Chrome瀏覽器記憶體模式
- java jvm 引數 -Xms -Xmx -Xmn -Xss 調優總結JavaJVM
- 減少.NET應用程式記憶體佔用的一則實踐記憶體
- MongoDB 如何使用記憶體?為什麼記憶體滿了?MongoDB記憶體
- MongoDB如何使用記憶體?為什麼記憶體滿了?MongoDB記憶體
- JVM記憶體分為3個記憶體空間JVM記憶體
- 詳解JVM中的記憶體模型是什麼?JVM記憶體模型
- AIX程式記憶體佔用數的計算AI記憶體
- JVM 記憶體模型 記憶體分配,JVM鎖JVM記憶體模型
- mysql用於分配記憶體的引數MySql記憶體
- 為什麼win10系統microsoft modules installer worker佔用記憶體較高Win10ROS記憶體
- Java記憶體模型是什麼,為什麼要有Java記憶體模型,Java記憶體模型解決了什麼問題?Java記憶體模型
- C語言中 struct成員變數順序對記憶體的佔用C語言Struct變數記憶體
- 如何檢視MySQL資料庫佔多大記憶體,佔用太多記憶體怎麼辦?MySql資料庫記憶體
- 修改oracle記憶體佔用Oracle記憶體
- 資源記憶體佔用記憶體
- Win10記憶體佔用過多怎麼辦 win10清理記憶體佔用的方法Win10記憶體
- 電腦記憶體佔用過高怎麼辦 電腦記憶體佔用過高解決方法記憶體
- win10開機記憶體佔用高怎麼解決_win10開機後記憶體佔用高的解決措施Win10記憶體
- JVM記憶體JVM記憶體
- SQL Server為什麼這麼耗記憶體SQLServer記憶體
- Linux程式記憶體佔用數的計算Linux記憶體
- Win10開機後記憶體佔用高80%以上怎麼回事 Win10開機後記憶體佔用高80%以上的處理方法Win10記憶體
- win10正式版記憶體佔用高怎麼辦_win10正式版記憶體佔用突然變高如何解決Win10記憶體
- IBM P550 為什麼記憶體顯示比實際的要少?IBM記憶體
- 物件為什麼活在記憶體的解析物件記憶體