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