遺失的JVM堆內存
“HI,你能不能過來幫我看下這個奇怪的現象?”我之所以會寫這篇文章是因為我在一個技術支持的案例中遇到了這么一個情況。這個問題是由于不同的JVM工具所檢測出來的可用內存的大小不一致所產生的。
簡言之,就是有一個工程師在排查某個應用內存使用過多的問題,而他一直“認為”這個程序的堆是2G的。由于某些原因,JVM工具貌似也不太確定這個進程的堆到底有多大。比如說,jconsole認為這個堆的最大可用內存為1963M,而jvisualvm檢測出來的是2048m。那么到底哪個才是對的,為什么不同的工具會顯示出不同的結果呢?
這的確很蹊蹺,尤其是嫌疑最大的JVM也被排除掉了——JVM是沒有動過其它手腳的,因為:
- -Xmx與-Xms的配置值相等,因此在運行時堆增長的時候這個數值是不會變的。
- 由于關掉了自適應調整的策略(-XX:-UseAdaptiveSizePolicy),JVM也無法動態地調整內存池的大小。
問題重現
要弄清楚這個問題首先得看一下實現的工具本身。要獲取可用內存的信息,最簡單的方式就是下面這種了:
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當前可用內存的大小。如果它發現內存大小發生了變化,它會將Runtime.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的源碼中發現了這么一段代碼 :
// Support for java.lang.Runtime.maxMemory(): return the maximum amount of // memory that the vm could make available for storing 'normal' java objects. // This is based on the reserved address space, but should not include space // that the vm uses internally for bookkeeping or temporary storage // (e.g., in the case of the young gen, one of the survivor // spaces). virtual size_t max_capacity() const = 0;
不得不說這實在是太隱蔽了。不過線索還是有的,只有那些真正好奇的人才能發現——真相就是在計算堆大小的時候,其中的一個存活區在某些情況下可能會被排除在外。
這之后的事情就比較簡單了——打開GC日志后我們可以發現,在2G的堆下,Serial, Parallel以及CMS算法所設置的存活區的大小都恰好是內存缺失的這部分。比如說,上例中的這個ParallelGC的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區的大小是524,800K,兩個存活區是87,040K,而老生代的大小是1,398,272K。將Eden區以及老生代,再加上一個存活區的大小,正好就是2,010,112K,也就是說缺失的這85M或者說87,040K,的確就是剩下的那一個存活區。
總結
讀完本文后你會對Java API的實現有一個新的認識。如果下次JVM工具將可用堆的總內存可視化時比-Xmx中配置的要小了那么一點點的話,你就知道這是少了其中的一個存活區了。
當然我也承認,這在日常的開發工作中并沒有什么實際用途,但這并不是本文的重點。事實上,本文想說的是,通常來說,我認為一名優秀的工程師應該具備的一個特征就是——好奇心。一個優秀的工程師應當時刻保持著一探究竟的熱情。有時候答案可能很隱蔽,但我還是建議你嘗試去把它找出來。你這一路所收獲到的知識最終一定會回饋給你的。
原創文章轉載請注明出處:遺失的JVM堆內存