为引用变量赋值时的 GC 行为

2022-09-02 13:00:53

我试图理解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);          
    }
}

在上面的例子中,如果我分配给null,那么我得到.如果我不这样做,我会得到.holdLastObjectTotal no of object garbage collected=910

有人可以解释一下吗?我无法找到正确的原因。


答案 1

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

当你赋值局部变量时,正如Jon Skeet所提到的,这是一个确定的赋值,javac必须在方法中创建一个局部变量,正如字节码所证明的那样:nullmain

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

在这种情况下,局部变量将保留最后分配的值,并且仅在超出范围时才可用于垃圾回收。由于它在其中定义仅在程序终止时才超出范围,因此在打印时,不会收集它。maini

如果你给它赋值,因为它永远不会在循环之外使用,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)

答案 2

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

如果将值赋给循环之前,则肯定会为整个方法分配该值(从声明点开始) - 因此,即使您在循环之后不访问它,GC 也会理解您可能已经编写了访问它的代码,因此它不会最终确定最后一个实例。holdLastObject

由于您没有在循环之前为变量赋值,因此除了在循环中之外,它不会明确分配 - 所以我怀疑GC将其视为在循环中声明 - 它知道循环之后的任何代码都不能从变量中读取(因为它没有明确分配),因此它知道它可以完成并收集最后一个实例。

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

System.out.println(holdLastObject);

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

我怀疑这是一个VM细节 - 我希望如果GC可以证明没有代码实际上将从局部变量中读取,那么无论如何它收集最终实例都是合法的(即使它目前没有以这种方式实现)。

编辑:与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 */

和这个围巾:

  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 */

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