Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GC behavior when assigning null to reference variable

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.

like image 430
Rajeev Avatar asked Mar 10 '15 06:03

Rajeev


People also ask

Can you assign null to this reference variable?

null can only be assigned to reference type, you cannot assign null to primitive variables e.g. int, double, float, or boolean.

What does it mean to reference a variable that is null?

In Java programming, null can be assigned to any variable of a reference type (that is, a non-primitive type) to indicate that the variable does not refer to any object or array.

Is null eligible for garbage collection?

Not necessarily. An object becomes eligible for garbage collection when there are no live threads anymore that hold a reference to the object. Explicit nulling is simply the practice of setting reference objects to null when you are finished with them.

What is the benefit of setting a variable to null when you no longer need it?

Explicitly assigning a null value to variables that are no longer needed helps the garbage collector to identify the parts of memory that can be safely reclaimed.


3 Answers

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)
like image 121
Mureinik Avatar answered Sep 28 '22 11:09

Mureinik


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...

like image 23
Jon Skeet Avatar answered Sep 28 '22 11:09

Jon Skeet


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.

like image 44
TheLostMind Avatar answered Sep 28 '22 11:09

TheLostMind