遗失的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;

不得不说这实在是太隐蔽了。不过线索还是有的,只有那些真正好奇的人才能发现——真相就是在计算堆大小的时候,其中的一个存活区在某些情况下可能会被排除在外

image

这之后的事情就比较简单了——打开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中配置的要小了那么一点点的话,你就知道这是少了其中的一个存活区了。

当然我也承认,这在日常的开发工作中并没有什么实际用途,但这并不是本文的重点。事实上,本文想说的是,通常来说,我认为一名优秀的工程师应该具备的一个特征就是——好奇心。一个优秀的工程师应当时刻保持着一探究竟的热情。有时候答案可能很隐蔽,但我还是建议你尝试去把它找出来。你这一路所收获到的知识最终一定会回馈给你的。

基于STM32F407,使用DFS算法实现最短迷宫路径检索,分为三种模式:1.DEBUG模式,2. 训练模式,3. 主程序模式 ,DEBUG模式主要分析bug,测量必要数据,训练模式用于DFS算法训练最短路径,并将最短路径以链表形式存储Flash, 主程序模式从Flash中….zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值