这的确是个挺奇怪的问题,特地是当最常呈现的几种解释理由都被排除后,看来JVM并没有耍一些显著的小花招:

  • -Xmx和-Xms是相等的,因而检测后果并不会因为堆内存减少而在运行时有所变动。
  • 通过敞开自适应调整策略(-XX:-UseAdaptiveSizePolicy),JVM曾经当时被禁止动静调整内存池的大小。

重现差别检测后果

要弄清楚这个问题的第一步就是要明确这些工具的实现原理。通过规范APIs,咱们能够用以下简略语句失去可应用的内存信息。

System.out.println("Runtime.getRuntime().maxMemory()="+Runtime.getRuntime().maxMemory());

而且的确,现有检测工具底层也是用这个语句来进行检测。要解决这个问题,首先咱们须要一个可重复使用的测试用例。因而,我写了上面这段代码:

package eu.plumbr.test;//imports skipped for brevitypublic 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、分布式缓存、数据结构等等。

我必须抵赖这个知识点在日常编程中并不是特地罕用,但这并不是这篇帖子的重点。我写下这篇帖子是为了形容一种特质,一种我常常在优良的程序员身上寻找的特质-好奇心。好的程序员们会常常试着去理解一些事物工作的机理以及起因。有时问题的答案并不会那么不言而喻,然而心愿你能保持寻找上来,最终在寻找过程中的所累积的常识总会让你获益匪浅。