【问题标题】:Single linked list still consuming memory after emptied [duplicate]清空后单链表仍然消耗内存[重复]
【发布时间】:2018-12-10 22:44:26
【问题描述】:

对于一项作业,我们被要求使用单链表实现数据结构。

我的问题是,在添加项目然后删除它们之后,java 程序仍然使用与以前相同的内存。

这是一个简单的例子

Deque<Integer> deck = new Deque<>();
for( int i =0; i < 500000;i++) {
    deck.addFirst(i);
}

for( int i =0; i < 500000;i++) {
    deck.removeFirst();
}
System.out.println("end"); //Debugger here

添加 50 万个项目会消耗 27mb 的内存。 删除它们后仍为 27mb。 最后用调试器跳转,变量deck有字段first = null 和计数 = 0;

为什么会这样?甲板是空的,没有项目,但内存仍然像以前一样使用。

代码通过了所有的正确性测试,但在内存测试中失败了。

我还查看了逐步调试器,并且正在做应该做的事情。

可能是在 removeFirst() 我没有将任何内容设置为 null 而只是将 first 分配为 first.next 吗?

编辑:这是内存测试的输出

 Test 10: Total memory usage after inserting 4096 items, then successively
     deleting items, seeking values of n where memory usage is maximized
     as a function of n

               n        bytes
    ---------------------------------------------------
  => passed     3200        65592         
  => passed     1600        65592         
  => FAILED      800        65592   (1.7x)
  => FAILED      400        65592   (3.4x)
  => FAILED      200        65592   (6.7x)
  => FAILED      100        65592  (13.1x)
  => FAILED       50        65592  (25.3x)
  ==> 2/7 tests passed

如您所见,有 50 个元素仍在使用 65592

编辑2:

// remove and return the item from the front
public Item removeFirst() {
    if (this.isEmpty()) throw new java.util.NoSuchElementException();
    Item current = first.Item;
    first = first.Next;
    count--;
    return current;
}

这是 removeFirst()

编辑3:

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;

/**
 * Created by Alex on 6/30/2018.
 */

public class Deque<Item> implements Iterable<Item> {

    private Node first;

    private int count;

    private class Node {
        private Node Next;
        private Item Item;
    }

    // construct an empty deque
    public Deque() {
        first = null;
        count = 0;
    }

    // is the deque empty?
    public boolean isEmpty() {
        if (count == 0) {
            return true;
        }
        return false;
    }

    // return the number of items on the deque
    public int size() {
        return count;
    }

    // add the item to the front
    public void addFirst(Item item) {
        if (item == null) throw new IllegalArgumentException();
        Node temp = new Node();
        temp.Item = item;
        temp.Next = first;
        first = temp;
        count++;
    }

    // add the item to the end
    public void addLast(Item item) {
        if (item == null) throw new IllegalArgumentException();
        if (isEmpty()) {
            addFirst(item);
        } else {
            Node last = first;
            while (last.Next != null) {
                last = last.Next;
            }
            Node newLast = new Node();
            newLast.Item = item;
            newLast.Next = null;
            last.Next = newLast;
            count++;
        }
    }

    // remove and return the item from the front
    public Item removeFirst() {
        if (this.isEmpty()) throw new java.util.NoSuchElementException();
        Item current = first.Item;
        first = first.Next;
        count--;
        return current;
    }

    // remove and return the item from the end
    public Item removeLast() {
        if (isEmpty()) throw new java.util.NoSuchElementException();
        if (size() == 1) {
           return removeFirst();
        }else {
            Node newLast = first;
            Node oldLast = first;
            while (oldLast.Next != null) {
                newLast = oldLast;
                oldLast = oldLast.Next;
            }
            newLast.Next = null;
            count--;
          //  Item lastItem = ;
            return oldLast.Item;
        }
    }

    // return an iterator over items in order from front to end
    public Iterator<Item> iterator() {
        return new DequeIterator();
    }

    private void debug() {
        Iterator<Item> deckIter = iterator();
        while(deckIter.hasNext()) {
            System.out.println(deckIter.next());
        }
        System.out.println(isEmpty());
        System.out.println("Size:" + size());
    }

    // an iterator, doesn't implement remove() since it's optional
    private class DequeIterator implements Iterator<Item> {
        private Node current = first;
        public boolean hasNext()  { return current != null;                     }
        public void remove()      { throw new UnsupportedOperationException();  }

        public Item next() {
            if (!hasNext()) throw new NoSuchElementException();
            Item item = current.Item;
            current = current.Next;
            return item;
        }
    }

    // unit testing (optional)
    public static void main(String[] args) throws InterruptedException {
        Deque<Integer> deck = new Deque<>();

        for( int i =0; i < 500000;i++) {
            deck.addFirst(i);
        }

        for( int i =0; i < 500000;i++) {
            deck.removeFirst();
        }
        System.out.println("end");
        TimeUnit.MINUTES.sleep(5);
    }
}

Edit4:这些是内存限制

https://imgur.com/nQuDfUF

谢谢。

【问题讨论】:

  • Java 会在需要的时候获取内存;不早于。您如何确定使用了多少内存?另外,“正确性测试”是如何定义的?
  • 我正在查看 Windows 上的任务管理器。编辑:正确性测试只是测试程序的输出,我将添加内存测试的输出
  • 您能展示一下您的removeFirst() 实现吗?我也想知道那个测试用例是怎么实现的。
  • @FoxAlex Java 在程序结束之前可能不会向操作系统释放内存。不过,该内存将可用于new 运算符的未来分配。这是一个性能特征。 Java 可以(或应该能够)比操作系统更快地为new 请求获取内存。
  • stackoverflow.com/questions/1582209/…stackoverflow.com/questions/30458195/…。 (可能是重复的?我认为我们不能比那些问答封面更多地推测这一点。)

标签: java memory data-structures singly-linked-list


【解决方案1】:

从我的测试看来,内存仍然被分配,但正在等待垃圾回收。 java.lang.Runtime 类有一些方法可以通知您内存使用情况,还有一个静态方法 gc() 建议运行垃圾收集。这是一个额外的方法和你的 main() 类的修改,可能会对你有所帮助:

private final static long MB = 1024*1024;
static void memStats(String when)
{
    Runtime rt = Runtime.getRuntime();
    long max = rt.maxMemory();
    long tot = rt.totalMemory();
    long free = rt.freeMemory();
    System.out.printf(
        "mem(mb): %5d tot, %5d free %5d used %5d max %s\n",
         tot/MB, free/MB, (tot-free)/MB, max/MB, when);
}

// unit testing (optional)
public static void main(String[] args) {
    Deque<Integer> deck = new Deque<>();
    memStats("startup");
    for( int i =0; i < 500000;i++) {
        deck.addFirst(i);
    }
    memStats("after alloc");

    for( int i =0; i < 500000;i++) {
        deck.removeFirst();
    }
    memStats("after removal");
    Runtime.getRuntime().gc();
    memStats("after gc");

    System.out.println("end");
    try {
        TimeUnit.MINUTES.sleep(5);
    } catch (InterruptedException ex)
    {
    }
}

将它们放在 Deque 类中的 main() 位置并运行它。我明白了:

mem(mb):    15 tot,    15 free     0 used   247 max startup
mem(mb):    29 tot,    10 free    19 used   247 max after alloc
mem(mb):    29 tot,    10 free    19 used   247 max after removal
mem(mb):    29 tot,    29 free     0 used   247 max after gc
end

如您所见,(1) 分配给 Java 的内存从 Java 对象的总分配 15MB 开始,使用了 0MB。 (忽略max 数字。这并没有报告我的想法。)它增加到 29MB,分配后使用了 19。

立即释放Deque中的所有Nodes,完全没有变化!

不过,在 gc() 调用之后,请注意分配给堆的总内存并没有改变,但现在所有的内存都可供其他 Java 对象使用。垃圾回收最终会发生——通常是在堆中没有足够的连续内存来满足new 请求时。

正如我在 cmets 中所说,我对添加到堆中的额外 14MB 左右没有返回给操作系统并不感到惊讶。如果您在 Windows 中使用任务管理器,该内存仍将被视为“正在使用”。去操作系统获取内存是很昂贵的,所以通常这样做只是为了扩展大块(在这种情况下似乎是大约 1500 万字节)并且只在运行结束时才释放。不过,这是依赖于实现的行为,并且在不同的 JVM 上可能会有所不同。


关于您的代码的注释。我将 Node 类转换为:

private static class Node<Item> {
    Node<Item> next; // lowercase next
    Item item; // You were using Item for both a type and field name!?
}

...并将几乎所有出现的Node 转换为Node&lt;Item&gt;。这是应该的,因为节点项类型是通用的。在同一份报告中,这将内存使用从 19MB 减少到 15MB。非静态内部类的每个内部类实例都与周围类的特定实例相关联。你不需要那个,它会耗费时间和内存。

该修复可能会帮助您通过测试。

哦,是的,请注意字段名称从 Next 更改为 next 并且 Item 更改为 item。如果您想适应传统的 Java 命名风格,请以小写字母开头字段和方法名称。在任何情况下,您都不应该将 Item 用于字段名称和通用类型名称。

【讨论】:

  • 感谢您的回答,我不明白其他人如何通过单链表通过此测试...
  • @FoxAlex 你从来没有解释过测试是什么。但是,您可能需要注意,链表比数组消耗更多空间,并且使用非静态内部类来降低 Node 成本。
  • 所有关于测试的信息都在编辑的帖子中,我不知道更多。
猜你喜欢
  • 2018-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多