[英]Java - why does assigning “null” to variable does not make it available for GC? (In this code snippet)
[英]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
。
有人可以解釋一下嗎? 我無法找到正確的理由。
檢查字節碼有助於揭示答案。
當您為局部變量賦值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
的整個塊,因此證明了這一理論是正確的。
注意:
據我所知,這種行為不是由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)
我懷疑這是由於明確的任務。
如果在循環之前為holdLastObject
賦值,那么它肯定是為整個方法分配的(從聲明之后開始) - 所以即使你在循環之后沒有訪問它,GC也知道你可以編寫代碼訪問它,因此它不會完成最后一個實例。
因為你沒有在循環之前為變量賦值,所以除了在循環中之外沒有明確賦值 - 所以我懷疑GC將它視為在循環中聲明它 - 它知道循環之后沒有代碼可以讀取從變量(因為它沒有明確分配),所以它知道它可以完成並收集最后一個實例。
只是為了澄清我的意思,如果你添加:
System.out.println(holdLastObject);
就在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 */
這與分配:
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,因此它在循環外部創建 ,因此null reference
被推入其局部變量表中的槽中。 因此,JVM理解該字段可以從外部訪問 ,因此它不會破壞它保持活動的最后一個實例 ,因為它仍然是可訪問/可讀的 。因此,除非您明確地將最后一個引用的值設置為null,否則它存在並且可以訪問。 因此,9個實例將為GC做好准備。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.