19

I was trying to understand the behavior of GC and I found something that interests me which I am unable to understand.

Please see the code and output:

public class GCTest {
    private static int i=0;

    @Override
    protected void finalize() throws Throwable {
        i++; //counting garbage collected objects
    }

    public static void main(String[] args) {        
        GCTest holdLastObject; //If I assign null here then no of eligible objects are 9 otherwise 10.

        for (int i = 0; i < 10; i++) {            
             holdLastObject=new GCTest();             
        }

        System.gc(); //requesting GC

        //sleeping for a while to run after GC.
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // final output 
        System.out.println("`Total no of object garbage collected=`"+i);          
    }
}

In above example if I assign holdLastObject to null then I get Total no of object garbage collected=9. If I do not, I get 10.

Can someone explain it? I am unable to find the correct reason.

1
  • you should extract your test into a separate function and execute that in a loop. JIT will kick in at some point and optimize out all kinds of things, e.g. via escape analysis. A single run is insufficient to observe all effects. Commented Mar 10, 2015 at 14:35

3 Answers 3

11

Examining the bytecode helps reveal the answer.

When you assign null to the local variable, as Jon Skeet mentioned, this is a definite assignment, and javac must create a local variable in the main method., as the bytecode proves:

// access flags 0x9
public static main([Ljava/lang/String;)V
  TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException
 L3
  LINENUMBER 12 L3
  ACONST_NULL
  ASTORE 1

In this case, the local variable will keep the last assigned value and will only be available for garbage collection when it goes out of scope. Since it's defined in main it only goes out of scope when the program is terminated, at the time you print i, it isn't collected.

If you do not assign a value to it, since it's never used outside the loop, javac optimizes it to a local variable in the for loop's scope, which can of course be collected before the program terminates.

Examining the bytecode for this scenario shows that the entire block for LINENUMBER 12 is missing, hence proving this theory right.

Note:
As far as I know, this behavior is not defined by the Java standard, and may vary between javac implementations. I've observed it with the following version:

mureinik@computer ~/src/untracked $ javac -version
javac 1.8.0_31
mureinik@computer ~/src/untracked $ java -version
openjdk version "1.8.0_31"
OpenJDK Runtime Environment (build 1.8.0_31-b13)
OpenJDK 64-Bit Server VM (build 25.31-b07, mixed mode)
Sign up to request clarification or add additional context in comments.

Comments

9

I suspect it's due to definite assignment.

If you assign a value to holdLastObject before the loop, it's definitely assigned for the whole method (from the point of declaration onwards) - so even though you don't access it after the loop, the GC understands that you could have written code that accessses it, so it doesn't finalize the last instance.

As you don't assign a value to the variable before the loop, it's not definitely assigned except within the loop - so I suspect the GC treats it as if it were declared in the loop - it knows that no code after the loop could read from the variable (because it's not definitely assigned) and so it knows it can finalize and collect the last instance.

Just to clarify what I mean by this, if you add:

System.out.println(holdLastObject);

just before the System.gc() line, you'll find it won't compile in your first case (without the assignment).

I suspect this is a VM detail though - I'd hope that if the GC could prove that no code was actually going to read from the local variable, it would be legal for it to collect the final instance anyway (even if it isn't implemented that way at the moment).

EDIT: Contrary to TheLostMind's answer, I believe the compiler gives this information to the JVM. Using javap -verbose GCTest I found this without the assignment:

  StackMapTable: number_of_entries = 4
    frame_type = 253 /* append */
      offset_delta = 2
      locals = [ top, int ]
    frame_type = 249 /* chop */
      offset_delta = 19
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/InterruptedException ]
    frame_type = 4 /* same */

and this with the assigment:

  StackMapTable: number_of_entries = 4
    frame_type = 253 /* append */
      offset_delta = 4
      locals = [ class GCTest, int ]
    frame_type = 250 /* chop */
      offset_delta = 19
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/InterruptedException ]
    frame_type = 4 /* same */

Note the difference in the locals part of the first entry. It's odd that the class GCTest entry doesn't appear anywhere without the initial assignment...

10 Comments

Admittedly I don't know whether the compiler indicates in the bytecode where a variable is definitely assigned, or whether the VM works that out itself.. - Well, it actually pushes a null reference into the variable in the local variable table in case you initialize it to null. :P
with null --> 0: aconst_null 1: astore_1 .. Without null --> 0: iconst_0 1: istore_2
@TheLostMind: Yes, but what's the difference between doing that in the loop vs doing it outside the loop, for example - in terms of the bytecode?
@TheLostMind: Only that the distinction isn't relevant in many cases. I view it all as one big black box for the most part. (Likewise where is the boundary of the JIT, exactly?) I'm not bothered - whereas other information such as whether the JVM is inferring this information or whether it's using what's in the byte code is a more concrete difference. Anyway, I think we should stop the chat at this point...
@Amitd: When running a release mode build, and not under the debugger, the .NET JIT can be pretty aggressive - it notices when there's no more access to a local variable and ignores it as a GC root. Heck, it can also collect an object while an instance method is still running in that object, if it can prove that no code path can read a field...
|
6

I didn't find any major differences in the byte code for both cases (so not worth posting the byte code here). So my assumption is that this is due to JIT / JVM optimizations.

Explanation :

case -1 :

public static void main(String[] args) {
  GCTest holdLastObject; //If I assign null here then no of eligible objects are 9 otherwise 10.
     for (int i = 0; i < 10; i++) {
         holdLastObject=new GCTest();
    }
    //System.out.println(holdLastObject); You can't do this here. holdLastObject might not have been initialized.
     System.gc(); //requesting GC
}

Here, note that you have not initialized holdLastObject to null. So, outside the loop, it cannot be accessed (you will get a compile time error). This means that *the JVM figures out that the field is not being used in the later part. Eclipse gives you that message. So, the JVM will create and desroy everything inside the loop itself. So, 10 objects Gone.

Case -2 :

 public static void main(String[] args) {
      GCTest holdLastObject=null; //If I assign null here then no of eligible objects are 9 otherwise 10.
         for (int i = 0; i < 10; i++) {
             holdLastObject=new GCTest();
        }
        //System.out.println(holdLastObject); You can't do this here. holdLastObject might not have been initialized.
         System.gc(); //requesting GC
    }

In this case, since the field is initialized to null, it is created outside the loop and hence a null reference is pushed into its slot in the local variables table. Thus the JVM understands that the field is accessible from outside so it does not destroy the last instance it keeps it alive as it is still accessible /readable.So unless you explictly set the value of the last reference to null, it exists and is reachable. Hence 9 instances will be ready for GC.

4 Comments

It's odd that the JVM doesn't figure out that the variable isn't being used after the loop at all though - it can do so much, but it really can't spot that there are no reads of the field after the loop?
@JonSkeet - Well. In the second case, there is a probability of that happening. Haven't checked if escape analysis was enabled when I tested. Gotto see the result with escape analysis.
@TheLostMind: Not in the bytecode - it can surely analyze that and tell that there is no possible path that will read the field. I've just used javap -verbose myself, and found there is another difference, in the StackMapTable. Still investigating what that means.
@JonSkeet - On which version on Java?. That makes a difference :P . check here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.