2012-01-11 54 views
12

我在閱讀「CLR via C#」,看起來在本例中,最初分配給'obj'的對象在執行第1行之後將有資格進行垃圾收集,而不是第2行。Java與.Net中的對象生命週期

void Foo() 
{ 
    Object obj = new Object(); 
    obj = null; 
} 

這是因爲局部變量使用壽命不是由定義的範圍定義的,而是由您上次讀取的。

所以我的問題是:Java呢?我編寫了這個程序來檢查這種行爲,並且它看起來像對象仍然活着。我不認爲JVM在解釋字節碼時可能會限制變量的生命週期,所以我試圖用'java -Xcomp'運行程序來強制編譯方法,但'finalize'不會被調用。看起來對於Java來說這不是真的,但我希望我能在這裏得到更準確的答案。另外,Android的Dalvik VM呢?

class TestProgram { 

    public static void main(String[] args) { 
     TestProgram ref = new TestProgram(); 
     System.gc(); 
    } 

    @Override 
    protected void finalize() { 
     System.out.println("finalized"); 
    } 
} 

補充: 傑弗裏裏希特給出了代碼示例在 「通過C#CLR」,這樣的事情:

public static void Main (string[] args) 
{ 
    var timer = new Timer(TimerCallback, null, 0, 1000); // call every second 
    Console.ReadLine(); 
} 

public static void TimerCallback(Object o) 
{ 
    Console.WriteLine("Callback!"); 
    GC.Collect(); 
} 

TimerCallback對MS的.Net只調用一次,如果項目的目標是 '釋放'(定時器在調用GC.Collect()後銷燬),並且如果目標是'Debug'(變量壽命增加,因爲程序員可以嘗試使用調試器訪問對象),則每秒調用一次。但是,無論您如何編譯它,每秒都會調用Mono回調函數。看起來Mono的'Timer'實現存儲對線程池某處實例的引用。 MS實施不這樣做。

+10

我以前在Java規範中找不到這個。請注意,.NET可能比你期望的更瘋狂 - 當實例方法正在執行時,可能收集實例*如果CLR知道將不會引用更多實例變量。 – 2012-01-11 17:15:02

回答

4

注意,僅僅因爲一個對象可以收集,並不意味着它實際上將被收集在任何給定的點 - 所以你的方法可以給假陰性。如果任何對象的finalize方法被調用,你可以肯定地說它是無法訪問的,但如果方法沒有被調用,你不能從邏輯上推斷任何東西。與大多數與GC相關的問題一樣,垃圾收集器的非確定性使得很難提出有關它將做什麼的測試/保證。

在到達性/可收回的話題,JLS說(12.6.1):

一個可達對象是可以在任何可能繼續計算從任何活動線程訪問的任何對象。優化程序的轉換可以設計爲將可達到的對象數量減少到小於天真被認爲可達的對象數量。例如,編譯器或代碼生成器可以選擇將不再用於空的變量或參數設置爲使得此類對象的存儲器可以更快地被回收。

這或多或少是你所期望的正是 - 我覺得上面一段是與同構「的對象是不可達當且僅當你絕對不會用它了。」

回到原來的情況,您能想到在第1行之後被視爲無法訪問的對象與第2行之間的任何實際影響?我最初的反應是沒有,如果你以某種方式設法找到這樣的情況,它可能是壞/扭曲代碼的標誌,導致虛擬機掙扎而不是語言的固有弱點。

雖然我願意反駁。


編輯:感謝有趣的例子。

我同意你的評估,看看你要去哪裏,但問題可能更多的是調試模式微妙地改變了你的代碼的語義。

在編寫的代碼中,您將Timer分配給一個局部變量,該變量隨後不會在其範圍內進行讀取。即使是最平凡的逃逸分析也可以揭示變量在main方法的其他地方沒有使用,因此可以省略。因此,我認爲你的第一線可以被認爲是完全等同於只需要直接調用構造函數:

public static void Main (string[] args) 
{ 
    new Timer(TimerCallback, null, 0, 1000); // call every second 
    ... 

在後一種情況下,很顯然,新創建Timer對象是不可達立即施工後(假設它不」不要做任何鬼鬼祟祟的事情,比如將自己添加到靜態字段等構造函數中);並且一旦GC收到它,它就會被收集起來。

現在在調試的情況下,事情是微妙的不同,因爲你提到的原因是開發人員可能希望稍後在方法中檢查局部變量的狀態。因此編譯器(和JIT編譯器)不能優化這些;就好像在方法的末尾有一個變量的訪問,直到那個點阻止收集。

即便如此,我不認爲這實際上改變了語義。 GC的本質是集合很少得到保證(至少在Java中,唯一的保證是,如果OutOfMemoryError被拋出,那麼所有被認爲無法訪問的東西都會被預先立即調用)。事實上,假設您有足夠的堆空間來存放運行時生命週期內創建的每個對象,則無操作的GC實現是完全有效的。因此,雖然您可能會觀察到Timer刻度的行爲變化,但這沒有問題,因爲根據調用方式不能保證您會看到什麼。 (這在概念上類似於在系統負載情況下運行整個CPU密集型任務的計時器將多次打勾 - 由於該界面不提供這種保證,所以結果都是錯誤的。)

在這一點我把你引回到這個答案的第一句話。 :)

+0

感謝您的回覆。我將編輯我的問題並在那裏添加代碼示例。 – Ivan 2012-01-11 17:54:18

+1

我在我的文章中添加了代碼示例,它顯示了不同實現的不同程序行爲。網絡和不同的構建目標,由該優化引起。 – Ivan 2012-01-11 18:27:38

+0

底層環境中的變量壽命!=語言中的變量,這種行爲是不直觀的,理論上可以破壞代碼,儘管我不認爲這確實是一個問題。只是想知道Java規範是否說明了這種情況。使用GC每天看起來越來越複雜:) – Ivan 2012-01-12 16:43:44

1

Java通常具有這樣的行爲,即如果對象在範圍內可訪問(存在對該對象的引用而非垃圾),則該對象不是垃圾。這是遞歸的,所以如果a是對引用b的對象的引用,則對象b指的不是垃圾。

在範圍內,你仍然可以達到通過ref引用的對象,(你可以添加一個行System.out.println(ref.toString())),ref是不是垃圾。

但是,根據this old source from Sun's site,大部分依賴於JVM的具體實現。