【问题标题】:memory allocation in collection of 1 million references in javajava中100万个引用集合中的内存分配
【发布时间】:2016-09-10 16:49:53
【问题描述】:

我创建了一个包含 100 万个 MyItem 对象的 ArrayList,消耗的内存为 106mb(从任务管理器检查)但是通过 addAll() 方法将相同的列表添加到另外两个列表后,它需要 259mb。我的问题是我只添加了对 list 的引用,在那一百万之后没有创建新对象。为什么即使使用了 LinkedList 内存消耗也会增加(因为它不需要连续的内存块,所以不会进行重新分配)?

如何有效地实现这一目标?数据通过我的程序中的各种列表并消耗超过 1GB 的内存。上面介绍了类似的场景。

public class MyItem{
private String s;
private int id;
private String search;

public MyItem(String s, int id) {
    this.s = s;
    this.id = id;
}

public String getS() {
    return s;
}

public int getId() {
    return id;
}


public String getSearchParameter() {
    return search;
}

public void setSearchParameter(String s) {
    search = s;
}
}

public class Main{
    public static void main(String args[]) {
        List<MyItem> l = new ArrayList<>();
        List<MyItem> list = new LinkedList<>();
        List<MyItem> list1 = new LinkedList<>();

        for (int i = 0; i < 1000000 ; i++) {
            MyItem m = new MyItem("hello "+i ,i+1);
            m.setSearchParameter(m.getS());
            l.add(i,m);
        }

        list.addAll(l);

        list1.addAll(l);
        list1.addAll(list);

        Scanner s = new Scanner(System.in);
        s.next();//just not to terminate 
    }
}

【问题讨论】:

  • ArrayList 以数组的名称为基础。如果数组变得太小一个新数组,则创建双倍大小并将引用复制到新数组。由于 Java 有自己的内存管理,第一个数组的内存将仅在 JVM 内部释放。

标签: java memory-management arraylist reference linked-list


【解决方案1】:

LinkedList是一个doubly-linked list,所以列表中的元素用节点表示,每个节点包含3个引用。

从 Java 8 开始:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

由于您使用了大量内存,您可能没有使用压缩的 OOP,因此引用可能是 64 位的,即每个 8 字节。

对于 16 字节 + 每个引用 8 字节的对象头,一个节点占用 40 字节。如果有 100 万个元素,那就是 40 Mb。

两个列表是 80 Mb,然后记住 Java 内存被分割成池并且对象(节点)被移动,现在额外 153 Mb 的内存消耗似乎是正确的。

注意:Arraylist 每个元素只使用 8 个字节,而不是 40 个字节,如果您预先分配后备数组,因为您知道大小,您可以这样做,这样可以节省大量内存。

【讨论】:

  • 由于你使用了大量内存,你可能没有使用压缩的 OOP,如果堆小于 32 GB,默认情况下 UseCompressedOops 在 64 位 JVM 上启用,如果这里不是这种情况,我会感到惊讶,因为这只是一个简单的测试,你不同意吗?
【解决方案2】:

无论何时您在后台调用 LinkedList.addAll,它都会为每个添加的元素创建一个 LinkedList.Node,因此您在这里创建了 300 万个这样的节点,这确实不是免费的:

  1. 这个对象有 3 个引用,知道引用的大小是 4 bytes on 32-bit JVM64-bit JVM 并启用了 UseCompressedOops (-XX:+UseCompressedOops),默认情况下,堆小于Java 7 及更高版本中的32 GB64-bit JVM 上的8 bytes 禁用UseCompressedOops (-XX:-UseCompressedOops)。所以这里根据你的配置给出 12 bytes24 bytes
  2. 然后我们在32-bit JVM64-bit JVM上添加标题字段的大小,即8 bytes16 bytes。所以这里根据你的配置给出 8 bytes16 bytes

所以如果我们总结一下:

  1. 32-bit JVM 上每个实例 20 字节
  2. 28 字节64-bit JVM 上的每个实例,启用UseCompressedOops
  3. 40 字节64-bit JVM 上的每个实例,UseCompressedOops 禁用

当您在 LinkedList 上调用 3 次 addAll 的 100 万个对象时,它给出了

  1. 60 个月 on 32-bit JVM
  2. 84 个月64-bit JVM 上启用UseCompressedOops
  3. 120 个月64-bit JVM 上禁用 UseCompressedOops

其余的可能是垃圾收集器尚未收集的对象,您应该在加载您的ArrayList 后尝试调用System.gc() 以获取实际大小,并在加载您的LinkedList 后执行相同的操作。

如果要获取给定对象的大小,可以使用SizeOf

如果您使用64-bit JVM 并且想知道UseCompressedOops 是否已启用,只需在仅带有-X 选项的终端中启动您的java 命令并添加-XX:+PrintFlagsFinal | grep UseCompressedOops 例如如果我的命令是@987654357 @,启动java -Xms4g -Xmx4g -XX:MaxPermSize=4g -XX:+PrintFlagsFinal | grep UseCompressedOops,输出的开头应该是这样的:

     bool UseCompressedOops                        := true            {lp64_product}
     ...

在这种情况下,标志 UseCompressedOops 已启用

【讨论】:

  • 我以为 LinkedList.Node 将被分配参考!如果不是,我怎么能在不分配更多节点的情况下工作;(感谢您的回复!
  • 它就是这样做的,但是它为每个添加的元素创建一个 LinkedList.Node 的实例以保留前一个和下一个节点
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-02
  • 1970-01-01
  • 2015-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-28
相关资源
最近更新 更多