java System.gc() only works if I set obj=null implicitly?

98 views Asked by At

I'm using Java 8 on windows 10 and I have this code snippet to test how System.gc() works:

public class testGc{
    static class MyObject{
        @Override
        protected void finalize() throws Throwable{ // not called when System.gc()
            System.out.println("gc: " + toString());
        }
    }

    public static void main(String[] args) throws Exception{
        {
            MyObject obj=new MyObject();
            System.out.println("NOT set to null");
        }
        System.gc(); // obj still exists
        Thread.sleep(500);
    }
}

You can see in my main(), the code block ends and obj is no longer referenced, and then System.gc(). I expect that this "obj" is recycled. But this program only prints:

NOT set to null
public static void main(String[] args) throws Exception{
    {
        MyObject obj=new MyObject();
        System.out.println("Set to null");
        obj=null;//need this expression!
    }
    System.gc(); // now it works
    Thread.sleep(500);
}

Now the program prints:

Set to null
gc: testGc$MyObject@2130772

Why do I need to use "=null" to make System.gc() work and call my "finalize" function? The "obj" is no longer valid after the code block.

How to understand this?

1

There are 1 answers

1
rzwitserloot On

Because that's not how the JVM works.

local variables, at the JVM (class file) level, do not exist. Instead there's the stack (class files are a stack based language), and any method sets up a fixed amount of 'slots', that are unnamed. There is only a light 'link' between local variables and slots. For example, this code:

void example() {
 int a = 5;
 System.out.println(a);
 int b = 10;
 System.out.println(b);
}

will end up using only a single slot (or probably even no slots whatsoever, with the literals pushed straight onto the stack, and the println method will then consume them).

Any object that is still 'pointed at' by a local 'slot' in an active method cannot be garbage collected. Slots don't disappear. So in your example code, yes, in java (the language) terms, The obj variable no longer exists, but in java (the virtual machine) terms, it does - that method has 1 slot, and the first slot is pointing at that object. javac is not going to decide to add additional code just to undo this fact to aid garbage collection - the billions of memory writes this would entail would be almost entirely useless and would dwarf the gains!

Instead when a method exits, its stack space and those slots all get reused (and marked away as no longer preventing garbage collection in a much smarter fashion that explicitly writing over them), and that's 'fast enough' for pretty much all intents and purposes.

This does mean that in extremely rare cases where you do something like this:

void example() {
  {
    List<String> list = ... make some GIGANTIC 1GB+ list ...
    process(list);
  }

  callSomeMethodThatAlsoNeedsTonsOfMemory();
}

you're going to run into memory errors, and it is wise to add an explicit list = null; in the middle. Except, that's not actually true - what's much better is to take the 'block' as written above and stick that in a method, then replace this method with just 2 calls. Now there's no need for that - the fact that the method containing the 1GB+ list (and the process(list) call) ends before callSomeMethodThatAlsoNeedsTonsOfMemory begins means its slots are cleared as normal.

And to go up even further - it's probably unwise to make a gigantic list and then process it, just to garbage collect it immediately afterwards. There are frightfully few algorithms where that makes sense - most likely you either wanted to process this data unit-by-unit (instead of generating 1 million entries, sticking them in a list, then processing the list - in a loop generate 1 entry, process it, then generate the next, process that, and so on), or, hey if it takes 1GB it will probably take 100GB if you scale a bit more, so this needs a disk-backed database solution instead.

In other words, it is entirely intentional that the JVM can in these exotic circumstances fail to GC when it could have.

NB: System.gc() is merely a request; a hint of sorts. It doesn't guarantee that garbage collection will actually occur. However, it usually 'works', and the explanation for what you are witnessing has nothing to do with the fact that System.gc isn't a guarantee.