做者:Nikita Salnikov-Tarnovski 譯者:Amanda 校對:java
「你好,你能過來看看幫我解決一個奇怪的問題麼。」就是這個技術支持案例使我想起寫下這篇帖子。眼前的這個問題就是關於不一樣工具對於可用內存大小檢測的差別。程序員
其實就是一個工程師在調查一個應用程序的太高的內存使用狀況時發現,儘管該程序已經被指定分配2G堆內存,可是JVM檢測工具彷佛並不能肯定進程實際能用多少內存。例如jconsole顯示可用堆內存爲1,963M,然而jvisualvm 卻顯示能用2,048M。因此到底哪一個工具纔是對的,爲何檢測結果會出現差別呢?算法
這確實是個挺奇怪的問題,特別是當最常出現的幾種解釋理由都被排除後,看來JVM並無耍一些明顯的小花招:編程
-Xmx和-Xms是相等的,所以檢測結果並不會由於堆內存增長而在運行時有所變化。ide
經過關閉自適應調整策略(-XX:-UseAdaptiveSizePolicy),JVM已經事先被禁止動態調整內存池的大小。
工具
重現差別檢測結果測試
要弄清楚這個問題的第一步就是要明白這些工具的實現原理。經過標準APIs,咱們能夠用如下簡單語句獲得可以使用的內存信息。spa
System.out.println("Runtime.getRuntime().maxMemory()="+Runtime.getRuntime().maxMemory());rest
複製代碼orm
並且確實,現有檢測工具底層也是用這個語句來進行檢測。要解決這個問題,首先咱們須要一個可重複使用的測試用例。所以,我寫了下面這段代碼:
package eu.plumbr.test;
//imports skipped for brevity
public class HeapSizeDifferences {
static Collection objects = new ArrayList();
static long lastMaxMemory = 0;
public static void main(String[] args) {
try {
List 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[1_000_000]置於一個循環中來不斷分配內存給程序,而後監測JVM運行期的當前可用內存。當程序監測到可用內存大小發生變化時,經過打印出Runtime.getRuntime().maxMemory()返回值來獲得當前可用內存尺寸,輸出相似下面語句:
Running with: [-Xms2048M, -Xmx2048M]
Runtime.getRuntime().maxMemory(): 2,010,112K.
複製代碼
實際狀況也確實如預估的那樣,儘管我已經給JVM預先指定分配了2G對內存,在不知道爲何在運行期有85M內存不見了。你大能夠把 Runtime.getRuntime().maxMemory()的返回值2,010,112K 除以1024來轉換成MB,那樣你將獲得1,963M,正好和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這個類的源代碼中找到了以下的解釋:
Running with: [-Xms2048M, -Xmx2048M]
// 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;
複製代碼
我不得不說這個答案藏得有點深,可是隻要你有足夠的好奇心,仍是不難發現的:有時候,有一塊Survivor區是不被計算到可用內存中的。
明白這一點以後問題就好解決了。打開並查看GC logging 信息以後咱們發現,在Serial,Parallel以及CMS算法回收過程當中丟失的那些內存,尺寸恰好等於JVM從2G堆內存中劃分給Survivor區內存的尺寸。例如,在上面的ParallelGC算法運行時,GC logging信息以下:
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,兩個Survivor區都被分配到了87,040K,老年代(Old space)則被分配了1,398,272K。把Eden區、老年代以及一個Survivor區的尺寸求和,恰好等於2,010,112K,說明丟失的那85M(87,040K)確實就是剩下的那個Survivor區。
總結
讀完這篇帖子的你如今應該對如何探索Java API的實現原理有了一些新的想法。下次當你用某個可視化工具查看可用堆內存發現所得的結果略少於-Xmx指定分配的大小時,你就知道這二者之間的差值是一塊Survivor區的大小。
我必須認可這個知識點在平常編程中並非特別經常使用,但這並非這篇帖子的重點。我寫下這篇帖子是爲了描述一種特質,一種我常常在優秀的程序員身上尋找的特質-好奇心。好的程序員們會常常試着去了解一些事物工做的機理以及緣由。有時問題的答案並不會那麼顯而易見,可是但願你能堅持尋找下去,最終在尋找過程當中的所累積的知識總會讓你獲益匪淺。
原創文章,轉載請註明: 轉載自ifeve.com