将 null 分配给引用变量时的 GC 行为
GC behavior when assigning null to reference variable
我试图了解 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
。
谁能解释一下?我找不到正确的原因。
我怀疑这是由于明确的分配。
如果你在循环之前给 holdLastObject
赋值,它肯定是为整个方法赋值的(从声明点开始)——所以即使你在循环之后没有访问它, GC 知道您可能 编写了访问它的代码,因此它不会完成最后一个实例。
因为你不在循环之前为变量赋值,所以它没有明确赋值,除非在循环内 - 所以我怀疑 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 */
注意第一个条目的 locals
部分的区别。奇怪的是 class GCTest
条目没有出现 任何地方 没有初始分配...
我没有发现这两种情况的字节码有任何重大差异(因此不值得在此处发布字节码)。所以我的假设是这是由于 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
}
这里,注意你还没有将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 reference
被推入其在 局部变量 table 中的位置。因此 JVM 知道该字段是 可从外部访问的 所以它不会 销毁最后一个实例 它保持它仍然存在,因为它仍然是 accessible /readable. 所以除非你显式设置最后一个引用的值为null,否则它是存在的并且是可达的。因此 9 个实例将为 GC 做好准备。
检查字节码有助于揭示答案。
当你将 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
循环范围内的局部变量,当然可以在程序终止之前收集。
检查此场景的字节码表明缺少 LINENUMBER 12
的整个块,因此证明该理论是正确的。
注:
据我所知,这种行为 not 由 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)
我试图了解 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
。
谁能解释一下?我找不到正确的原因。
我怀疑这是由于明确的分配。
如果你在循环之前给 holdLastObject
赋值,它肯定是为整个方法赋值的(从声明点开始)——所以即使你在循环之后没有访问它, GC 知道您可能 编写了访问它的代码,因此它不会完成最后一个实例。
因为你不在循环之前为变量赋值,所以它没有明确赋值,除非在循环内 - 所以我怀疑 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 */
注意第一个条目的 locals
部分的区别。奇怪的是 class GCTest
条目没有出现 任何地方 没有初始分配...
我没有发现这两种情况的字节码有任何重大差异(因此不值得在此处发布字节码)。所以我的假设是这是由于 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
}
这里,注意你还没有将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 reference
被推入其在 局部变量 table 中的位置。因此 JVM 知道该字段是 可从外部访问的 所以它不会 销毁最后一个实例 它保持它仍然存在,因为它仍然是 accessible /readable. 所以除非你显式设置最后一个引用的值为null,否则它是存在的并且是可达的。因此 9 个实例将为 GC 做好准备。
检查字节码有助于揭示答案。
当你将 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
循环范围内的局部变量,当然可以在程序终止之前收集。
检查此场景的字节码表明缺少 LINENUMBER 12
的整个块,因此证明该理论是正确的。
注:
据我所知,这种行为 not 由 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)