Java字节数组1MB或更大会占用两倍的RAM。

14
在Windows 10 / OpenJDK 11.0.4_x64上运行以下代码会输出used: 197expected usage: 200,这意味着200个长度为100万的字节数组大约占用200MB RAM。一切都很好。
当我将代码中的字节数组分配从new byte[1000000]更改为new byte[1048576](即1024 * 1024元素),它会输出used: 417expected usage: 200。到底怎么回事?
import java.io.IOException;
import java.util.ArrayList;

public class Mem {
    private static Runtime rt = Runtime.getRuntime();
    private static long free() { return rt.maxMemory() - rt.totalMemory() + rt.freeMemory(); }
    public static void main(String[] args) throws InterruptedException, IOException {
        int blocks = 200;
        long initiallyFree = free();
        System.out.println("initially free: " + initiallyFree / 1000000);
        ArrayList<byte[]> data = new ArrayList<>();
        for (int n = 0; n < blocks; n++) { data.add(new byte[1000000]); }
        System.gc();
        Thread.sleep(2000);
        long remainingFree = free();
        System.out.println("remaining free: " + remainingFree / 1000000);
        System.out.println("used: " + (initiallyFree - remainingFree) / 1000000);
        System.out.println("expected usage: " + blocks);
        System.in.read();
    }
}

通过使用visualvm深入查看,我在第一个案例中看到了预期的一切:

byte arrays take up 200mb

在第二种情况下,除了字节数组外,我看到同样数量的 int 数组占用与字节数组相同的内存:

int arrays take up additional 200mb

这些 int 数组没有显示它们被引用,但我不能对它们进行垃圾回收...(字节数组显示在哪里被引用就行。)

这里发生了什么事情,有什么想法吗?


这可能与JVM内部使用int[]来模拟大型byte[]以获得更好的空间局部性有关吗? - Jacob G.
@JacobG。这肯定是内部问题,但在指南中似乎没有任何迹象。 - Kayaman
@second 是的,显然神奇的限制就是数组是否占用了1MB的RAM。我猜如果你只减去1,那么内存会被填充以提高运行时效率和/或数组计数的管理开销达到1MB...有趣的是JDK8的行为不同! - Georg
@Georg,不是很确定,我发现int或long数组也有同样的问题,所以这似乎比只有1MB更复杂。(对于longs,限制似乎是1024*1024 - 2而不是-16 - GotoFinal
@GotoFinal 2个long变量占用16字节,因此这没有什么区别,而16字节是任何对象的标准JVM头大小。因此,在分配超过1 MiB总堆大小的数组时,显然存在JVM内部限制。 - drekbour
显示剩余5条评论
1个回答

9
这段话描述的是G1垃圾收集器的开箱即用行为,它通常默认为1MB的“区域”,并在Java 9中成为JVM的默认值。启用其他GCs会得到不同的数字。
任何大于半个区域大小的对象都被认为是“庞大的”... 对于那些略大于堆区域大小的对象,这些未使用的空间可能导致堆变得分散。
我运行了java -Xmx300M -XX:+PrintGCDetails,它显示堆被“庞大的”区域耗尽了。
[0.202s][info   ][gc,heap        ] GC(51) Old regions: 1->1
[0.202s][info   ][gc,heap        ] GC(51) Archive regions: 2->2
[0.202s][info   ][gc,heap        ] GC(51) Humongous regions: 296->296
[0.202s][info   ][gc             ] GC(51) Pause Full (G1 Humongous Allocation) 297M->297M(300M) 1.935ms
[0.202s][info   ][gc,cpu         ] GC(51) User=0.01s Sys=0.00s Real=0.00s
...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

我们希望我们的1MiB byte[]小于一半的G1区域大小,因此添加-XX:G1HeapRegionSize=4M会使应用程序运行良好:

[0.161s][info   ][gc,heap        ] GC(19) Humongous regions: 0->0
[0.161s][info   ][gc,metaspace   ] GC(19) Metaspace: 320K->320K(1056768K)
[0.161s][info   ][gc             ] GC(19) Pause Full (System.gc()) 274M->204M(300M) 9.702ms
remaining free: 100
used: 209
expected usage: 200

G1深度概述:https://www.oracle.com/technical-resources/articles/java/g1gc.html G1详细信息:https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552

我在使用串行GC和长达8MB的数组时遇到了相同的问题(而在大小为1024-1024-2时很好),更改G1HeapRegionSize在我的情况下没有任何作用。 - GotoFinal
我并不清楚。能否请您用 long[] 作为参数,澄清一下上面代码中使用的 Java 调用和输出结果。 - drekbour
@GotoFinal,我没有观察到任何上述未解释的问题。我使用long[1024*1024]测试了代码,预期使用量为1600M。使用G1,根据-XX:G1HeapRegionSize变化[1M使用:1887,2M使用:2097,4M使用:3358,8M使用:3358,16M使用:3363,32M使用:1682]。使用-XX:+UseConcMarkSweepGC:1687。使用-XX:+UseZGC:2105。使用-XX:+UseSerialGC:1698。 - drekbour
只需像这样编写代码,而不更改任何GC选项,它将打印出used: 417 expected usage: 400,但如果我删除那个-2,它将变为used: 470,因此大约有50MB消失了,而且50 * 2 longs绝对不到50MB。 - GotoFinal
1
同样的事情。区别在于约50MB,然后您有50个 "巨大的" 块。这是GC详细信息: 1024 * 1024 -> [0.297s] [info] [gc,heap] GC(18)巨大区域:450-> 450 1024 * 1024-2 -> [0.292s] [info] [gc,heap] GC(20)巨大区域:400-> 400 此证明最后两个long强制G1分配另一个1MB区域来存储16字节。 - drekbour
我能理解,上次我在使用不同的GC/region大小时无法再现这个问题,所以似乎是我弄错了,你是对的。可能是因为我多次启动了相同的东西但并没有真正改变任何内容...虽然巨大的块是我最初怀疑的事情,但很快就放弃了它,因为还有其他问题。 - GotoFinal

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