【问题标题】:Java Stack implementation memory leakJava Stack 实现内存泄漏
【发布时间】:2016-08-08 20:13:42
【问题描述】:

所以我最近尝试使用 Java 中的单个链表实现 Stack,但我遇到了这个问题。如果我将 100000 个元素自然地推入堆栈,则内存会增加,因为堆栈需要 100000 个节点来存储对象。但是,当堆栈清空时(在 100,000 次弹出后),对这些节点及其内容的引用应超出范围,因此如果堆栈填满,内存使用量不应增长。但是,当我的代码中发生这种情况时,内存会翻倍。换句话说,pop 似乎没有充分删除引用或允许垃圾收集,我希望有人能告诉我原因。

这里是栈类

public class Stack<T> {
    private Node<T> top;
    private int size;
    private long limit;

    public boolean isEmpty() {
        return this.size == 0;
    }

    public void setStackLimit(long limit) {
        this.limit = limit;
    }

    public int size() {
        return size;
    }
    public Stack() {
        this(1000L);
    }
    public Stack(long limit) {
        this.size = 0;
        this.top = null;
        this.limit = limit;
    }
    public void push(T o) throws StackOverflowException{
        if (this.size == this.limit) throw new StackOverflowException("The stack overflowed");
        if (this.top == null) this.top = new Node<T>(o);
        else {
            Node<T> r = new Node<T>(o);
            Node<T> temp = this.top;
            this.top = r;
            this.top.setNext(temp);
        }
        this.size++;
    }
    @SafeVarargs
    public final void mpush(T... o) throws StackOverflowException{
        for (T ob: o) {
            this.push(ob);
        }
    }
    public T pop() throws EmptyStackException{
        if (this.top == null) throw new EmptyStackException("The stack is empty");
        else {
            T o = this.top.getPayload();
            this.top = this.top.getNext();
            this.size--;
            return o;
        }
    }

    public boolean empty() {
        while (!this.isEmpty()){
            try {
                this.pop();
            }
            catch (Exception e) {
                return false;
            }
        }
        return true;
    }

    public String printStack() {
        String stack = "";
        Node<T> temp = this.top;
        while (temp != null){
            if (temp.getPayload() instanceof Stack<?>) {
                Stack<?> stack2 = (Stack<?>)temp.getPayload();;
                stack = "| " + stack2.printStack()  + stack;
            } else
                stack = "|" + temp.getPayload().toString() + stack;
            temp = temp.getNext();
        }
        return stack.replaceAll("[| ]$","");
    }

    public T peek() throws EmptyStackException {
        if (this.top == null) throw new EmptyStackException("The stack is empty");
        else {
            T o = this.top.getPayload();
            return o;
        }
    }

    public boolean isFull() {
        return this.size == this.limit;
    }

    public Object[] toArray() {
        Object[] returnvalue = new Object[this.size];
        int index = this.size-1;
        Node<T> temp = this.top;
        while (index >= 0) {
                returnvalue[index--] = temp.getPayload();
                temp = temp.getNext();
        }
        return returnvalue;
    }

    public String toString() {
        try {
            return "<Stack object of size " + this.size() + " last element: " + this.peek().toString() + ">";
        } catch (Exception e) {
            return "<Empty stack object of size " + this.size() + ">";
        }

    }
}

这是Node的实现

public class Node<T> {
    private T payload;
    private Node<T> next;
    public Node(T payload) {
        this.payload = payload;
    }
    public Node<T> getNext(){
        return this.next;
    }
    public boolean hasNext() {
        return this.next != null;
    }
    public T getPayload() {
        return this.payload;
    }
    public void setNext(Node<T> n) {
        this.next = n;
    }
    public void setPayload(T o) {
        this.payload = o;
    }
}

【问题讨论】:

  • 这不是内存泄漏。请了解内存泄漏是什么意思。这只是因为 jvm 在进程中保存了你的内存。这并不意味着你的记忆丢失了。
  • 另外,在 gc 的预期中。没有任何 gc 实现在创建垃圾后立即收集垃圾的规则。
  • 我知道当引用被删除时,内存不会从java进程中释放,但不应该继续消耗内存。如果它不将 RAM 还给操作系统,那么它至少在重新填充后不会获得更多内存。
  • 假设您的引用丢失,jvm 可能会或可能不会决定 gc 您的垃圾,因此看到您的内存使用量增加是合理的。 100k 不是触发 gc 的尺度。
  • 100,000 个小对象并没有那么多内存,可能只有几兆字节。 GC 不运行是完全合理的。如果您只是想看看会发生什么,您可以尝试致电System.gc();。 (普通代码不应该调用System.gc();。通常你应该让GC 做它的事情而不用担心它。)在Java 中造成任何类似于泄漏的事情是非常困难的。 (examples)

标签: java memory memory-leaks stack


【解决方案1】:

除非遇到真正的问题(例如 OOM 错误),否则您真的不应该担心这些事情。但是,当您提到千兆字节的内存时,我开始感兴趣,并决定试一试。我使用了这段代码:

Stack<Integer> stack = new Stack<>(100_000_000L);
System.out.println(Runtime.getRuntime().maxMemory());
for (int j = 0; j < 100; ++j) {
    System.out.println(j + "th run");
    for (int i = 0; i < 10_000_000; ++i) {
        stack.push(i);
    }
    System.out.println(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
    Thread.sleep(10000);
    stack.empty();
    System.out.println(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
    Thread.sleep(10000);
}

(顺便说一句,有long 限制但int 大小有什么意义?)

第一行打印了 3795845120,我假设它对应于 4 GB 的内存(其中一些内存被分配给堆栈、类、JVM 对象和其他非常有用的东西)。然后它进入一个循环,我监控了内存分析器(无论 NetBeans 8.1 使用什么)和 Windows 任务管理器。首次运行分析器报告 10 M 对象 / 400 MB 内存,Windows 报告大约 500 MB 内存使用情况。到目前为止一切顺利。

在第 4 次运行时,分析器中大约有 15 M 个对象/600 MB 内存,但在任务管理器中大约有 1 GB。不过,不是 40 M 个对象!所以 GC 正在做它的工作,尽管速度很慢。

在第 8 次运行时,分析器报告了 25 M 个对象,内存增长到大约 1700 MB。但在第 9 次运行时,它又回到了 1500 万个对象并停留在那里。任务管理器报告的内存卡在 1700 MB 并且不再增加。

在其他地方,打印的代码

0th run
406432424
408400384
1th run
408267736
409295992
2th run
602312032
603027096
3th run
620573368
621308040
4th run
614213120
615099616
5th run
616810512
617419624
6th run
615366040
616072952
7th run
620580088
621587032
8th run
1020848024
1022640736
9th run
627436904
628500048

您可以清楚地看到 GC 工作。

从这个实验中,我猜想 Java 试图将内存消耗保持在给定最大限制的 50% 以内。我的盒子有 16 GB 或 RAM,所以我想这就是默认限制如此之高的原因(它甚至默认使用服务器 VM)。低于 50% 时,GC 仍在工作,只是内存没有返回给操作系统。

为了检验这个假设,我用-Xmx 1000M 运行了相同的代码。不过,结果有点令人惊讶:根据任务管理器,它保持在大约 950M。这意味着该算法并不像 50% 那样简单。它可能是针对硬件和环境进行优化的智能设备。

不过,我们可以有把握地得出结论,GC 工作正常(惊喜!)。顺便说一句,我真的认为您应该在发布问题之前完成我的几行代码,以显示一些研究工作。这并不像它在任何地方都很难或需要一些特殊技能。那么这个问题可能看起来更像“What is the memory management strategy of the JVM?”,谷歌搜索显示了很多有趣的链接。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-29
    • 2015-08-14
    • 2012-08-11
    相关资源
    最近更新 更多