我试图理解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);
}
}
在上面的例子中,如果我将holdLastObject
指定为null,那么我得到Total no of object garbage collected=9
。如果我不这样做,我会得到10
。
有人可以解释一下吗?我无法找到正确的理由。
答案 0 :(得分: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
在这种情况下,局部变量将保留最后一个赋值,并且只有在超出范围时才可用于垃圾收集。由于它在main
中定义,因此仅在程序终止时超出范围,在您打印i
时,它不会被收集。
如果您不为其分配值,因为它从未在循环外使用过,javac会将其优化为for
循环中的局部变量&#39;范围,当然可以在程序终止之前收集。
检查此场景的字节码表明缺少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)
答案 1 :(得分:9)
我怀疑这是由于明确的任务。
如果您在循环之前为holdLastObject
分配了一个值,那么它肯定会分配给整个方法(从声明开始点开始) - 所以即使您不在以后访问它在循环中,GC了解您可以编写访问它的代码,因此它不会最终确定最后一个实例。
由于你没有在循环之前为变量赋值,除了在循环中之外它没有明确赋值 - 所以我怀疑GC会把它视为好像它在循环中声明 - 它知道循环之后没有代码从变量读取(因为它没有明确分配),所以它知道它可以完成并收集最后一个实例。
只是为了澄清我的意思,如果你添加:
System.out.println(holdLastObject);
在System.gc()
行之前,你会发现它不会在你的第一个案例中编译(没有作业)。
我怀疑这是一个虚拟机细节 - 我希望如果 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 */
注意第一个条目的locals
部分的差异。奇怪的是class GCTest
条目在没有初始分配的情况下
答案 2 :(得分: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
}
此处 note 您尚未将holdLastObject
初始化为null
。因此,在循环之外,它无法访问(您将收到编译时错误)。这意味着* 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
}
在这种情况下,由于字段已初始化为null,因此在循环之外创建,因此null reference
被推入其插槽中局部变量表。因此,JVM从外部了解该字段是可访问的,因此它不会销毁最后一个实例它保持活着,因为它仍然是可访问/可读。因此,除非您明确地将最后一个引用的值设置为null,否则它存在并且可以访问。因此,9个实例将为GC做好准备。