0

Was discussing code clarity when another developer. He said using local variables increases memory use. We argued that they will be garbage collected. and especially a bad idea if logging statements calls a function that hits db/ other external resource.

But following sample code seems to support what he is saying - even after calling GC using jconsole, still see that the class Worker uses less memory than Worked2. Any ideas why?

Free memory : 124629976 - worker 124784720

package test;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class ObjectLife {

    public static void main(String[] args) {
        // simple multi thread to mimic web app
        // ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 3, 110, null, null);
        Executor pool = Executors.newFixedThreadPool(3);
        String type = "1";

        if (args.length > 0) {
            type = args[0];
        }
        Work w = null;
        if ("1".equals(type)) {
            w = new Worker();
        } else {
            w = new Worker2();
        }
        w.init(2);
        System.out.println("w type " + w.getClass().getName());
        Watch.me.print();
        pool.execute(w);
        pool.execute(Watch.me);

    }

}

class Watch implements Runnable {
    long prev = 0;
    static Watch me = new Watch();

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1100);
            } catch (InterruptedException e) {
                System.out.println("Intrpt thread " + e);
            }
            print();
        }
    }

    public void print() {
        Runtime r = Runtime.getRuntime();
        long free = r.freeMemory();
        System.out.println("Free " + free + ", delta " + (free - prev));
        System.out.println(", av " + r.maxMemory());
        prev = free;
    }

}

class Work implements Runnable {
    double val = 0;

    public void init(double val) {
        this.val = val;
    }

    void do2() {
    }

    @Override
    public void run() {
        int cnt = 0;
        while (++cnt < 175) {
            do2();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("Intrpt thread " + e);
            }
        }
        System.gc();
        System.out.println("Type, v3, : " + this.getClass().getName());
        Watch.me.print();

    }

}

class Worker extends Work {

    public void do2() {
        double local = ++val;
        double local2 = local * 2;
        System.out.println(" local " + local + " double " + local2);
    }
}

class Worker2 extends Work {

    public void do2() {
        System.out.println(" local " + ++val + " double " + (val * 2));

    }

}

Why does the class Worker take more memory - even after calling GC several times and then disconnecting Jconsole from process and let a few second pass? (check and print avaialable every 2 seconds.

Can see that the code is the same as super class of work and worker are the same except for do2() method

Note: I'm connecting from jconsole and calling GC after the Work loop is done. This GC call does work. Calls the MX bean and that can see available memory drop.

Side note : I even notice that if i disconnect jconsole from my app then wait 4-5 seconds - available memory goes up again (i guess over head of connecting to jconsole). At this time I make the measurement. Point is I do some work, wait for JVM to settle down then measure.

Video of running this program and freeing mem with jconsole https://www.youtube.com/watch?v=MadBdryX8uk&

jconsole is a free tol with JDK in the bin folder.

Edited class, here increased the loop count, and now the memory is same no matter which class is used!

Can someone else run it?

package test;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Run with param 1 and 2 or change type to "2" and "1" Available memory changes, used jconsole to change force garbage collection.
 * 
 * See video https://www.youtube.com/watch?v=MadBdryX8uk
 */
public class ObjectLife {

    public static void main(String[] args) {
        // simple multi thread to mimic web app
        // ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 3, 110, null, null);
        Executor pool = Executors.newFixedThreadPool(3);
        String type = "1";

        if (args.length > 0) {
            type = args[0];
        }
        Work w = null;
        if ("1".equals(type)) {
            w = new Worker();
        } else {
            w = new Worker2();
        }
        w.init(2);
        System.out.println("w type " + w.getClass().getName());
        Watch.me.print();
        pool.execute(w);
        pool.execute(Watch.me);

    }

}

class Watch implements Runnable {
    long prev = 0;
    private int dieCnt = -1;
    static Watch me = new Watch();

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1100);
            } catch (InterruptedException e) {
                System.out.println("Intrpt thread " + e);
            }
            if (dieCnt > -1) {
                dieCnt--;

                if (dieCnt == 0) {
                    System.exit(0);
                }
            }
            print();
        }
    }

    public void print() {
        Runtime r = Runtime.getRuntime();
        long free = r.freeMemory();
        System.out.println("Pr v6 Free " + free + ", delta " + (free - prev) + ", av " + r.maxMemory());
        prev = free;
    }

    public void countDown() {
        dieCnt = 3;

    }

}

class Work implements Runnable {
    double val = 0;
    double val3 = 0;

    public void init(double val) {
        this.val = val;
    }

    void do2() {
    }

    @Override
    public void run() {
        int cnt = 0;
        while (++cnt < 475) {
            do2();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("Intrpt thread " + e);
            }
        }
        System.gc();
        System.out.println("Type : " + this.getClass().getName());
        Watch.me.print();
        System.out.println("oink");
        try {
            Thread.sleep(9100);
        } catch (InterruptedException e) {
            System.out.println("Intrpt thread " + e);
        }
        Watch.me.countDown();

    }

}

class Worker extends Work {

    public void do2() {
        double local = ++val;
        double local2 = local * 2;
        System.out.println(" local " + local + " double " + local2);
        val3 = local2 + 1;
    }
}

class Worker2 extends Work {

    public void do2() {
        System.out.println(" local " + ++val + " double " + (val * 2));
        val3 = (val * 2) + 1;

    }

}
  • I know local vars are more readable. I like them. I argue for them.
  • Just curious about this behavior.
tgkprog
  • 4,493
  • 4
  • 41
  • 70
  • Your double variables in Worker use no heap memory at all, they live purely on the stack. – Thomas Stets Mar 09 '15 at 07:49
  • So how do you explain the difference in available memory? If you have 10 minutes can you run it on your system and change the type so you get both results? After the 100 ms loop in Work is done let it run for a minute and then note down number after 'Free' – tgkprog Mar 09 '15 at 08:21
  • Having the JVM "settling down" is already a false premise. Make a main method that consists of nothing but a very long sleep and monitor the memory. There is stuff going on all the time. – musiKk Mar 09 '15 at 08:26
  • well did the things in this video https://www.youtube.com/watch?v=MadBdryX8uk& over 10 times. same behaviour every time. when i monitor a tomcat web app with one simple servlet, similar behaviour. a JVM doing nothing, no timers, no real code running does settlle down at least per memory available. – tgkprog Mar 09 '15 at 08:33

3 Answers3

3

First of all, System.gc() might run the garbage collector; or not. This is really not a reliable way to test memory consumption.

See When does System.gc() do anything for details.

Next: keep in mind that there is a just-in-time compiler. The JIT works best for "normal Java" code. Very often, when developers try to be "smart" and write "more efficient Java code" - they end up worse than before - because the JIT is not able to deal with this "special" code efficiently.

Write your programs to be clear, and readable. They might be read hundreds of times over their lifetime; probably by many different people. So, what I am saying is: it is more important to keep your code easy to understand. It is fair to keep efficiency in mind; but only start to "optimize" when you run into real problems - and then do a proper analysis; for example using a profiler. "Avoiding local variables to save memory" is not something that you should do "in general".

Community
  • 1
  • 1
GhostCat
  • 137,827
  • 25
  • 176
  • 248
2

Local variables live on the stack, not the heap and therefore don't have anything to do with garbage collection. The terms "variable" and "garbage collection" aren't really in the same realm. For reference types the objects variables point to are subject to garbage collection. Since you use primitive types (double) this doesn't matter.

Yes, local variables increase the memory used for a stack frame by a tiny amount (8 bytes for double and long, the rest 4 bytes) but basing ANY kind of optimization around that is ludicrous. You should always base the decision of whether to use local variables or not based on readability of the code.

musiKk
  • 14,751
  • 4
  • 55
  • 82
  • So how do you explain the difference in available memory? – tgkprog Mar 09 '15 at 08:19
  • 2
    @tgkprog You cannot reasonably profile such things in Java. The layers of abstraction are so deep, there are a million possible reasons from the JIT to processor optimizations. – musiKk Mar 09 '15 at 08:22
  • 'Local variables live on the stack, not the heap and therefore don't have anything to do with garbage collection. ' can you explain more or point to a link? even if on stack does the gc not have to clear it? – tgkprog Mar 09 '15 at 12:11
  • 'base the decision of whether to use local variables or not based on readability of the code.' i agree that is where this discussion started. but now trying to understand this example – tgkprog Mar 09 '15 at 12:12
  • You could read about [call stacks](https://en.wikipedia.org/wiki/Call_stack). It's a pretty universal concept. The number of local variables of a function is fixed so the space required can be acquired and freed in constant time. Objects live on the heap, references to objects are not necessarily local to a function so you don't really know easily if they are still referenced somewhere. That's why they have to be gc'd regularly. – musiKk Mar 09 '15 at 12:20
  • Increased the count in loop. and now memory available is constant (without using jconsole). Get same with both Worker and Worker2 – tgkprog Mar 09 '15 at 13:09
0

I agree that local variables are very small in foot print. I argue for them. Purpose of this whole exercise was to show a client code reviewer that they are not expensive. He was concerned.

Anyway my example behaved as expected - local variables has no extra over head when run more than n times. In my case it was around 500.

So I want to self answer : Even with jconsole (using it to call GC) did not see the memory drop on GC. Might be due to the fact that there are levels of GC (full and partial).

But after a certain number of iterations, the JVM settles down and then memory used in both examples is same (with and without local variables). Like others have noted : this is not describable to study but then how does one argue on a large project about what code standards should be?

Some developers love to point out these small things when real issue is somewhere else. So some data helps to show that local variables are in face local and get garbage collected.

And if your memory use is already high and you fear its going to be pushed to limit then you need some other refactoring or scaling out.

tgkprog
  • 4,493
  • 4
  • 41
  • 70