這確實是個挺奇怪的問題,特別是當最常出現的幾種解釋理由都被排除后,看來JVM并沒有耍一些明顯的小花招:
- -Xmx和-Xms是相等的,因此檢測結果并不會因為堆記憶體增加而在運行時有所變化,
- 通過關閉自適應調整策略(-XX:-UseAdaptiveSizePolicy),JVM已經事先被禁止動態調整記憶體池的大小,
重現差異檢測結果
要弄清楚這個問題的第一步就是要明白這些工具的實作原理,通過標準APIs,我們可以用以下簡單陳述句得到可使用的記憶體資訊,
System.out.println("Runtime.getRuntime().maxMemory()="+Runtime.getRuntime().maxMemory());
而且確實,現有檢測工具底層也是用這個陳述句來進行檢測,要解決這個問題,首先我們需要一個可重復使用的測驗用例,因此,我寫了下面這段代碼:
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演算法,果然檢測結果也不盡相同,

除了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區的大小,
私信回復 資料 領取一線大廠Java面試題總結+阿里巴巴泰山手冊+各知識點學習思維導+一份300頁pdf檔案的Java核心知識點總結!
這些資料的內容都是面試時面試官必問的知識點,篇章包括了很多知識點,其中包括了有基礎知識、Java集合、JVM、多執行緒并發、spring原理、微服務、Netty 與RPC 、Kafka、日記、設計模式、Java演算法、資料庫、Zookeeper、分布式快取、資料結構等等,
我必須承認這個知識點在日常編程中并不是特別常用,但這并不是這篇帖子的重點,我寫下這篇帖子是為了描述一種特質,一種我經常在優秀的程式員身上尋找的特質-好奇心,好的程式員們會經常試著去了解一些事物作業的機理以及原因,有時問題的答案并不會那么顯而易見,但是希望你能堅持尋找下去,最終在尋找程序中的所累積的知識總會讓你獲益匪淺,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/84952.html
標籤:Java
