Java的RAM使用情况与任务管理器显示不符

6

我一直在尝试使用Java的JVM制作一个长度为1024^3(基本上是1GB)的字节数组。我使用任务管理器(查看进程)和以下代码段来测量数组创建前、创建后和垃圾收集器销毁后的内存使用情况:

public static void showMemory() {
    System.out.println("Memory used: "
            + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024.D * 1024.D) + "mB.");
}

上面的代码分别显示为2Mb,1029Mb和2Mb。-> 这看起来很正常。 然而,当查看任务管理器时,Java的RAM使用量一开始是2mb,然后上升到1052Mb,并保持在那里,即使片段显示为2Mb。
由于我希望Java使用最少的资源,我该如何解决这个问题?
编辑:
我已经做了一些研究并确定了要使用的术语。实际上,本地内存的值并不像堆内存的值那样,并且通常大于堆内存。是否有办法减少使用的本机内存,以便它接近堆内存?

1
请看 http://stackoverflow.com/q/2419247/1362755。 - the8472
你可以使用垃圾优先(G1)GC,它不仅会在需要时增加堆大小,而且还会在可能的情况下缩小它。请看我的答案以了解如何使用它。 - Markus Weninger
3个回答

12

结论:

使用垃圾优先(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); // Wait 1/2 second
        createArray();
        Thread.sleep(500); // Wait another 1/2 seconds
        System.gc(); // Force a GC, array should be cleaned
        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


有人知道是否有计划使G1 GC在不进行完整GC的情况下并发地收缩堆吗? - Piotr Kołaczkowski
@PiotrKołaczkowski:我不知道,但是一般的问题是:为什么应该呢? - Markus Weninger
因为使用G1 GC的主要目标之一是避免大的暂停。我不想暂停我的应用程序几秒钟来释放所有这些空闲区域给操作系统。 - Piotr Kołaczkowski
@PiotrKołaczkowski:我认为将空区域释放给操作系统只需要很少的时间。这些区域已经是空的,只需要释放它们。然而,我必须承认,目前我没有任何支持我的假设的度量标准。 可能要将此作为一个新问题开放,这里有一些真正的JVM专家。 - Markus Weninger

2
堆是JVM内存中的一个区域。一个JVM通常会额外使用200-400 MB的内存,用于共享库、代码、线程栈、直接内存和GUI组件等。因此,即使只有2MB的对象在使用,应用程序也可能预留了更多的内存。
“有没有一种方法可以减少本地内存的使用,使其接近堆内存?”您可以使用旧版本的Java,这些版本往往使用较少的内存、较小的最大堆和永久代,并使用较少的其他资源。

打击和恶意降低投票? - Peter Lawrey
在我看来,使用旧版Java并不是一个好的解决方案,为什么不换用底层垃圾收集器呢?我已经写了一个答案,有显著的改进。 - Markus Weninger
@MarkusWeninger 缩小堆大小会有所帮助,但通常引起最大担忧的是非堆内存使用。 - Peter Lawrey
1
是的,但在OP的情况下,我们可以看到问题是由于非收缩堆引起的,因为在他的1GB对象死亡后,堆大小保持不变。否则,在任务管理器中显示的RAM利用率应该会改回到之前分配大对象之前的某个位置。 - Markus Weninger

1
如果你使用G1垃圾回收器,可以减少堆大小,但本机内存大小不会减少。在某些应用程序中,分配的本机内存可能比实际堆更多。本机堆受到抖动的影响。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接