“嘿,你能来看看奇怪的东西吗?” 这就是我开始研究一个支持案例的方式,该案例将我引向了这篇博客文章。 当前的特殊问题与不同的工具报告了有关可用内存的不同数字有关。
简而言之,一位工程师正在研究特定应用程序的过多内存使用情况,据他所知,该应用程序可以使用2G的堆。 但是无论出于什么原因,JVM工具本身似乎都没有决定该进程真正拥有多少内存。 例如, jconsole猜测总可用堆等于1,963M,而jvisualvm声称其等于2,048M。 那么哪个工具是正确的,为什么另一个却显示不同的信息呢?
确实确实很奇怪,尤其是看到通常的可疑对象都被淘汰了– JVM并没有采取任何明显的技巧,例如:
- -Xmx和-Xms相等,因此在运行时堆增加期间报告的数字不会更改
- 通过关闭自适应大小调整策略( -XX:-UseAdaptiveSizePolicy ),防止JVM动态调整内存池大小。
重现差异
理解问题的第一步是放大工具实现。 通过标准API访问可用的内存信息非常简单,如下所示:
System.out.println("Runtime.getRuntime().maxMemory()="+Runtime.getRuntime().maxMemory());
确实,这就是手头工具似乎正在使用的工具。 回答此类问题的第一步是拥有可重现的测试用例。 为此,我编写了以下代码段:
package eu.plumbr.test;
//imports skipped for brevitypublic 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();}
}
该代码通过循环中的新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算法 | Runtime.getRuntime()。maxMemory() |
---|---|
-XX:+ UseSerialGC | 2,027,264千 |
-XX:+ UseParallelGC | 2,010,112千 |
-XX:+ UseConcMarkSweepGC | 2,063,104千 |
-XX:+ UseG1GC | 2,097,152千 |
除了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;
我不得不承认答案是非常隐蔽的。 但是仍然有一些真正的好奇心可以找到的暗示–指的是在某些情况下,堆大小计算中可能会排除一个幸存空间 。
从这里一直是顺风顺水–打开GC日志记录发现,实际上,使用2G堆,串行,并行和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,Old和一个幸存者空间加在一起总计为2,010,112K,这证实丢失的85M或87,040K确实是剩余的幸存者空间 。
摘要
阅读该文章后,您现在对Java API实施细节有了新的了解。 下次某些工具将总可用堆大小可视化为略小于Xmx指定的堆大小时,您会知道差异等于您的Survivor空间之一的大小。
我必须承认,这一事实在日常编程活动中并不是特别有用,但这不是该帖子的重点。 取而代之的是,我写了一篇文章,描述了我一直在优秀工程师中寻找的一个特殊特性- 好奇心 。 优秀的工程师一直在寻找了解事物如何以及为什么以某种方式起作用的方式。 有时答案仍然隐藏,但我仍然建议您尝试寻求答案。 最终,沿途积累的知识将开始带来红利。
翻译自: https://www.javacodegeeks.com/2015/02/jvm-access-less-memory-specified-via-xmx.html