将null赋给引用变量时的GC行为

Raj*_*eev 19 java garbage-collection

我试图理解GC的行为,我找到了一些让我感兴趣的东西,我无法理解.

请参阅代码和输出:

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);          
    }
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,如果我指定holdLastObject为null,那么我得到Total no of object garbage collected=9.如果我不这样做,我会10.

有人可以解释一下吗?我无法找到正确的理由.

Mur*_*nik 11

检查字节码有助于揭示答案.

当您分配null给局部变量时,正如Jon Skeet所提到的,这是一个明确的赋值,并且javac 必须main方法中创建一个局部变量.,因为字节码证明:

// 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
Run Code Online (Sandbox Code Playgroud)

在这种情况下,局部变量将保留最后一个赋值,并且只有在超出范围时才可用于垃圾收集.由于它的定义main仅在程序终止时超出范围,因此在您打印时i,它不会被收集.

如果你没有为它赋值,因为它从不在循环之外使用,javac将它优化为for循环范围内的局部变量,当然可以在程序终止之前收集它.

检查此场景的字节码表明LINENUMBER 12缺少整个块,从而证明了这一理论是正确的.

注意:
据我所知,这种行为不是由Java标准定义的,并且可能因javac实现而异.我用以下版本观察过它:

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)
Run Code Online (Sandbox Code Playgroud)


Jon*_*eet 9

我怀疑这是由于明确的任务.

如果你holdLastObject在循环之前给一个值赋值,它肯定是为整个方法分配的(从声明之后开始) - 所以即使你在循环之后没有访问它,GC也知道你可以编写访问的代码它,所以它没有完成最后一个实例.

因为你没有在循环之前为变量赋值,所以除了在循环中之外没有明确赋值 - 所以我怀疑GC将它视为在循环中声明它 - 它知道循环之后没有代码可以读取从变量(因为它没有明确分配),所以它知道它可以完成并收集最后一个实例.

只是为了澄清我的意思,如果你添加:

System.out.println(holdLastObject);
Run Code Online (Sandbox Code Playgroud)

就在这System.gc()行之前,你会发现它不会在你的第一种情况下编译(没有作业).

我怀疑这是一个虚拟机细节 - 我希望如果 GC可以证明没有代码实际上是从本地变量中读取的,那么无论如何收集最终实例都是合法的(即使它不是' t目前以这种方式实施).

编辑:与TheLostMind的答案相反,我相信编译器会将此信息提供给JVM.使用javap -verbose GCTest我发现这没有任务:

  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 */
Run Code Online (Sandbox Code Playgroud)

分配:

  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 */
Run Code Online (Sandbox Code Playgroud)

请注意locals第一个条目部分的差异.奇怪的是,class GCTest如果没有初始分配,条目就不会出现在任何地方 ......


The*_*ind 6

我没有发现两种情况的字节代码有任何重大差异(所以不值得在这里发布字节代码).所以我的假设是这是由于JIT/JVM优化.

说明:

情况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
}
Run Code Online (Sandbox Code Playgroud)

在这里,请注意您尚未初始化holdLastObjectnull.因此,在循环之外,它无法访问(您将收到编译时错误).这意味着*JVM发现该字段未在后面部分中使用.Eclipse为您提供该消息.因此,JVM将创建并消除循环内部的所有内容.所以,10个物体已经过时了.

案例-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
    }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,由于字段被初始化为null,因此它在循环外部创建,因此a null reference被推入其局部变量表中的槽中.因此,JVM理解该字段可以从外部访问,因此它不会破坏它保持活动的最后一个实例,因为它仍然是可访问/可读的.因此,除非您明确地将最后一个引用的值设置为null,否则它存在并且可以访问.因此,9个实例将为GC做好准备.