0

So I recently attempted to implement a Stack using a single linked list in Java and I have this problem. If I push 100000 elements to the stack naturally, the memory increases because the stack requires 100000 nodes to store the objects. However, when the stack empties (after 100,000 pops) the references to these Nodes and their content should fall out of scope so that if the stack fills back up the memory usage should not grow. However, when this happens in my code, the memory doubles. In other words, pop does not seem to sufficiently remove references or allow for garbage collection and I am hoping that someone can tell me why.

Here is the stack class

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() + ">";
        }

    }
}

and here is the implementation of 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;
    }
}
  • that's not memory leak. please get to know what memory leak means. it's just because jvm holding your memory inside of the process. it doesn't mean your memory is lost. – Jason Hu Apr 16 '16 at 23:53
  • 1
    also, in gc's prospective. there is no rule for any gc implementation to collect garbages as soon as they are created. – Jason Hu Apr 16 '16 at 23:54
  • I know that the memory doesn't get released from the java process when a reference gets deleted, but shouldn't not continue to consume memory. If it doesn't give the RAM back to the OS shouldn't it at least not gain more memory after it is refilled. –  Apr 16 '16 at 23:54
  • assuming your reference is lost, jvm may or may not decide to gc your garbage so it's reasonable to see your memory usage increases. 100k isn't a scale for triggering gc. – Jason Hu Apr 16 '16 at 23:57
  • 100,000 of what isn't a scale –  Apr 16 '16 at 23:57
  • 1
    100,000 small objects isn't that much memory, maybe a few megabytes. It's perfectly plausible that GC just doesn't run. You can try calling `System.gc();` if you just want to see what happens. (Normal code shouldn't call `System.gc();`. Normally you should just let the GC do its thing and not worry about it.) It's very difficult to cause anything resembling a leak in Java. ([examples](http://stackoverflow.com/q/6470651/2891664)) – Radiodef Apr 17 '16 at 00:30
  • It also doesn't happen if I go up to 10,000,000 which is almost a gigabyte so could please stop patronizing me and tell me if you see I way I can fix this like you're supposed to be doing on this website –  Apr 17 '16 at 02:50

1 Answers1

0

You really shouldn't worry about such things unless you hit a real problem (e. g. OOM error). When you said about a gigabyte of memory, I became interested, though, and decided to give it a go. I used this code:

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);
}

(By the way, what's the point of having a long limit but an int size?)

The first line printed 3795845120, which I assume corresponds to something like 4 GB of memory (with some memory being allocated for stack, classes, JVM objects and other very useful things). Then it went into a loop and I monitored both memory profiler (whatever NetBeans 8.1 uses for that) and Windows Task Manager. At first run the profiler reported 10 M objects / 400 MB of memory, and Windows reported about 500 MB of memory usage. So far so good.

At the 4th run it was like 15 M objects / 600 MB of memory in the profiler, but about 1 GB in the Task Manager. Still, not 40 M objects! So the GC is doing its work, albeit slowly.

At the 8th run the profiler reported 25 M objects and the memory grew up to about 1700 MB. But at the 9th run it went back to 15 M objects and stayed there. The memory reported by the Task Manager stuck at 1700 MB and never increased any more.

Elsewhere, the code printed

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

You can clearly see GC working.

From this experiment I guessed that Java tries to keep memory consumption within 50% of whatever the maximum limit it was given. My box has 16 GB or RAM, so I guess that's why the default limit is that high (it even uses the server VM by default). Below that 50%, the GC is still doing its job, only the memory isn't given back to OS.

To test this hypothesis, I ran the same code with -Xmx 1000M. The result was a bit surprising, though: it stayed at about 950M according to the Task Manager. Which means the algorithm is not as simple as just 50%. It's probably something smart and optimized for the hardware and environment.

Still, we can safely conclude that GC is working properly (surprise!). By the way, I really think you should have done something like my few lines of code before posting the question, to show some research effort. It's not like it was anywhere hard or required some special skills. Then the question would have probably looked more like “What is the memory management strategy of the JVM?”, googling for which shows a lot of interesting links.

Sergei Tachenov
  • 24,345
  • 8
  • 57
  • 73