关于java:他们都说JVM能实际使用的内存比Xmx指定的少这是为什么呢

33次阅读

共计 3425 个字符,预计需要花费 9 分钟才能阅读完成。

这的确是个挺奇怪的问题,特地是当最常呈现的几种解释理由都被排除后,看来 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、分布式缓存、数据结构等等。

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

正文完
 0