结论:
使用垃圾优先(G1)GC(Java 9中的默认GC),此垃圾收集器在进行垃圾收集时还会缩小堆大小(因此,总体上也会缩小使用的“本机内存”),与ParallelOldGC(Java 7和Java 8中的默认GC)相比,后者很少或从不缩小堆大小!
一般情况:
你的基本假设是错误的。
你假设你的代码片段显示的是堆大小。这是不正确的。它显示了堆利用率。这意味着“我的堆空间使用了多少?”。Runtime.getRuntime().totalMemory()
显示堆大小,Runtime.getRuntime().freeMemory()
显示可用堆大小,它们之差显示了堆利用(已用大小)。
你的堆从一个初始大小开始,使用0字节的利用率,因为还没有创建任何对象,并且有一个最大堆大小。最大堆大小描述了垃圾回收器允许调整堆的大小的范围(例如,如果没有足够的空间来存储非常大的对象)。
在创建空堆之后的下一步,会自动加载一些对象(类对象等),它们通常应该轻松适合初始堆大小中。
然后,您的代码开始运行并分配对象。一旦 Eden 空间没有更多空间 (堆被分为年轻代(Eden、survivor-from 和 survivor-to 空间)和老年代,如果您对这些细节感兴趣,请查找其他资源), 垃圾回收就会触发。
在垃圾回收期间,垃圾收集器可能决定调整堆大小(如上所述,在谈论最大堆大小时)。在您的情况下会发生这种情况,因为您的初始堆大小太小,无法容纳您的 1GB 对象。因此,堆大小会增加,介于初始堆大小和最大堆大小之间。
然后,在大对象死亡后,下一个 GC 可能会使堆变小,但它不必如此。为什么?因为它在最大堆大小以下,这就是 GC 关心的。某些垃圾回收算法会再次缩小堆,而有些则不会。
特别是对于 Java 7 和 Java 8 中默认的 GC“ParallelOldGC”,它几乎不会缩小堆。
如果您想要一个垃圾回收器在垃圾回收时尝试缩小堆大小,可以通过设置
-XX:+UseG1GC
Java标志来尝试
垃圾优先(G1)GC。
示例:
这将以字节形式打印出所有值。
您将获得一个概述,了解两个GC如何工作以及使用它们中的任何一个时使用多少空间。
System.out.println(String.format("Init:\t%,d",ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getInit()));
System.out.println(String.format("Max:\t%,d%n", ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax()));
Thread outputThread = new Thread(() -> {
try {
int i = 0;
for(;;) {
System.out.println(String.format("%dms\t->\tUsed:\t\t%,d", i, ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed()));
System.out.println(String.format("%dms\t->\tCommited:\t%,d", i, ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getCommitted()));
Thread.sleep(100);
i += 100;
}
} catch (Exception e) { }
});
Thread allocThread = new Thread(() -> {
try {
int val = 0;
Thread.sleep(500);
createArray();
Thread.sleep(500);
System.gc();
return;
} catch (Exception e) { }
});
outputThread.start();
allocThread.start();
createArray()
只是下面这个小方法:
private static void createArray() {
byte[] arr = new byte[1024 * 1024 * 1024];
}
--结果 ParallelOldGC:
Init: 262,144,000
Max: 3,715,629,056
0ms -> Used: 6,606,272
0ms -> Commited: 251,658,240
100ms -> Used: 6,606,272
100ms -> Commited: 251,658,240
200ms -> Used: 6,606,272
200ms -> Commited: 251,658,240
300ms -> Used: 6,606,272
300ms -> Commited: 251,658,240
400ms -> Used: 6,606,272
400ms -> Commited: 251,658,240
500ms -> Used: 1,080,348,112
500ms -> Commited: 1,325,924,352
600ms -> Used: 1,080,348,112
600ms -> Commited: 1,325,924,352
700ms -> Used: 1,080,348,112
700ms -> Commited: 1,325,924,352
800ms -> Used: 1,080,348,112
800ms -> Commited: 1,325,924,352
900ms -> Used: 1,080,348,112
900ms -> Commited: 1,325,924,352
1000ms -> Used: 1,080,348,112
1000ms -> Commited: 1,325,924,352
1100ms -> Used: 1,080,348,112
1100ms -> Commited: 1,325,924,352
1200ms -> Used: 2,261,768
1200ms -> Commited: 1,325,924,352
1300ms -> Used: 2,261,768
1300ms -> Commited: 1,325,924,352
您可以看到,我的堆开始时的初始大小约为260MB,允许的最大大小(GC可能决定调整堆的大小)约为3.7GB。
在创建数组之前,我的堆使用了约6MB。然后创建了大数组,我的堆大小(已提交大小)增加到1.3GB,其中使用了约1GB(数组)。然后我强制进行垃圾回收,回收了数组。然而,我的堆大小仍然是1.3GB,因为GC不关心再次收缩它,只有利用率降低了2MB。
--结果 G1:
Init: 262,144,000
Max: 4,179,623,936
0ms -> Used: 2,097,152
0ms -> Commited: 262,144,000
100ms -> Used: 2,097,152
100ms -> Commited: 262,144,000
200ms -> Used: 2,097,152
200ms -> Commited: 262,144,000
300ms -> Used: 2,097,152
300ms -> Commited: 262,144,000
400ms -> Used: 2,097,152
400ms -> Commited: 262,144,000
500ms -> Used: 1,074,364,464
500ms -> Commited: 1,336,934,400
600ms -> Used: 1,074,364,464
600ms -> Commited: 1,336,934,400
700ms -> Used: 1,074,364,464
700ms -> Commited: 1,336,934,400
800ms -> Used: 1,074,364,464
800ms -> Commited: 1,336,934,400
900ms -> Used: 1,074,364,464
900ms -> Commited: 1,336,934,400
1000ms -> Used: 492,520
1000ms -> Commited: 8,388,608
1100ms -> Used: 492,520
1100ms -> Commited: 8,388,608
1200ms -> Used: 492,520
1200ms -> Commited: 8,388,608
好的,我们开始吧!G1 GC关注小堆!在对象被清理后,不仅利用率下降到约0.5MB,而且堆大小缩小到8MB(与ParallelOldGC中的1.3GB相比)。
更多信息:
但是,请记住,堆大小仍将与任务管理器中显示的不同。以下图片来自Wikipedia-Java虚拟机,说明堆只是完整JVM内存的一部分:
![JVM memory](https://istack.dev59.com/9NpX6.webp)