首頁 > Java > java教程 > 主體

簡單理解Java的垃圾回收機制與finalize方法的作用

高洛峰
發布: 2017-01-17 15:45:49
原創
1255 人瀏覽過

垃圾回收器要回收物件的時候,首先要呼叫這個類別的finalize方法(你可以寫程式驗證這個結論),一般的純Java所寫的Class不需要重新覆寫這個方法,因為Object已經實作了一個預設的,除非我們要實現特殊的功能(這裡面涉及到很多東西,例如物件空間樹等內容)。
不過用Java以外的程式碼寫的Class(例如JNI,C++的new方法分配的記憶體),垃圾回收器並不能對這些部分進行正確的回收,這時就需要我們覆蓋預設的方法來實現對這部分記憶體的正確釋放和回收(例如C++需要delete)。 
總之,finalize相當於析構函數,他是垃圾回收器回收一個物件的時候第一個要呼叫的方法。不過由於Java的垃圾回收機制能自動為我們做這些事情,所以我們在一般情況下是不需要自己來手工釋放的。

有時當撤銷一個物件時,需要完成一些操作。例如,如果一個物件正在處理的是非Java 資源,例如檔案句柄或window 字元字體,這時你要確認在一個物件被撤銷先前要保證這些資源被釋放。為處理這樣的狀況,Java 提供了被稱為收尾(finalization )的機制。使用該機制你可以定義一些特殊的操作,這些操作在一個物件將要被垃圾回收程式釋放時執行。 
要為一個類別增加收尾(finalizer ),你只要定義finalize ( ) 方法即可。 Java 回收該類別的一個物件時,就會呼叫這個方法。在finalize ( )方法中,你要指定在一個物件被撤銷前必須執行的操作。垃圾回收週期性地運行,檢查物件不再被運行狀態引用或間接地透過其他物件引用。就在物件被釋放之 前,Java 執行系統呼叫該物件的finalize( ) 方法。

  finalize()方法的通用格式如下:

  protected void finalize( )
{
// finalization code here
}
登入後複製

  其中,關鍵字protected是防止在該類別之外定義的程式碼存取finalize()識別碼。該標識符和其他標識符將在第7章中解釋。

  理解finalize( ) 正好在垃圾回收以前被調用非常重要。例如當一個物件超出了它的作用域時,finalize( ) 並不被呼叫。這意味著你不可能知道何時——甚至是否——finalize( ) 被調用。因此,你的程式應該提供其他的方法來釋放由物件使用的系統資源,而不能依賴finalize( ) 來完成程式的正常操作。

  注意:如果你熟悉C++,那你知道C++允許你為一個類別定義一個撤消函數(destructor ),它在物件正好出作用域之前被呼叫。 Java不支援這個想法也不提供撤銷函數。 finalize() 方法只和撤銷函數的功能接近。當你對Java 有豐富經驗時,你將看到因為Java使用垃圾回收子系統,幾乎沒有必要使用撤銷函數。


finalize的工作原理應該是這樣的:一旦垃圾收集器準備好釋放對象佔用的存儲空間,它首先調用finalize(),而且只有在下一次垃圾收集過程中,才會真正回收對象的內存.所以如果使用finalize(),就可以在垃圾收集期間進行一些重要的清除或清掃工作.

finalize()在什麼時候被調用?
有三種情況

所有物件被Garbage Collection時自動調用,比如運行System .gc()的時候.

程式退出時為每個物件呼叫一次finalize方法。

顯式的呼叫finalize方法

除此以外,正常情況下,當某個物件被系統收集為無用資訊的時候,finalize()將被自動呼叫,但是jvm不保證finalize()一定被呼叫,也就是說,finalize()的呼叫是不確定的,這也就是為什麼sun不提倡使用finalize()的原因

有時當撤消一個物件時,需要完成一些操作。例如,如果一個物件正在處理的是非Java 資源,例如檔案句柄或window 字元字體,這時你要確認在一個物件被撤銷先前要保證這些資源被釋放。為處理這樣的狀況,Java 提供了被稱為收尾(finalization )的機制。使用該機制你可以定義一些特殊的操作,這些操作在一個物件將要被垃圾回收程式釋放時執行。

要為一個類別增加收尾(finalizer ),你只要定義finalize ( ) 方法即可。 Java 回收該類別的一個物件時,就會呼叫這個方法。在finalize ( )方法中,你要指定在一個物件被撤銷前必須執行的操作。垃圾回收週期性地運行,檢查物件不再被運行狀態引用或間接地透過其他物件引用。就在物件被釋放之 前,Java 執行系統呼叫該物件的finalize( ) 方法。

finalize()方法的通用格式如下:

protected void finalize( )
{
// finalization code here
}
登入後複製

其中,關鍵字protected是防止在該類別之外定義的程式碼存取finalize()識別碼。該標識符和其他標識符將在第7章中解釋。

理解finalize( ) 正好在垃圾回收以前被呼叫非常重要。例如當一個物件超出了它的作用域時,finalize( ) 並不被呼叫。這意味著你不可能知道何時——甚至是否——finalize( ) 被調用。因此,你的程式應該提供其他的方法來釋放由物件使用的系統資源,而不能依賴finalize( ) 來完成程式的正常操作。

注意:如果你熟悉C++,那你知道C++允許你為一個類別定義一個撤消函數(destructor ),它在物件正好出作用域之前被呼叫。 Java不支援這個想法也不提供撤銷函數。 finalize() 方法只和撤銷函數的功能接近。當你對Java 有豐富經驗時,你將看到因為Java使用垃圾回收子系統,幾乎沒有必要使用撤銷函數。

垃圾收集器在進行垃圾收集的時候會自動呼叫物件的finalize方法,用來進行一些使用者自訂的非記憶體清理工作,因為垃圾收集器不會處理記憶體以外的東西。所以,有的時候使用者需要定義一些清理的方法,比如說​​處理檔案和連接埠之類的非記憶體資源。

1.JVM的gc概述
  
  gc即垃圾收集機制是指jvm用於釋放那些不再使用的物件所佔用的記憶體。 java語言並沒有要求jvm有gc,也沒有規定gc如何運作。不過常用的jvm都有gc,而且大多數gc都使用類似的演算法管理記憶體和執行收集操作。
  
  在充分理解了垃圾收集演算法和執行過程後,才能有效的優化它的性能。有些垃圾收集專用於特殊的應用程式。例如,即時應用程式主要是為了避免垃圾收集中斷,而大多數OLTP應用程式則注重整體效率。了解應用程式的工作負載和jvm支援的垃圾收集演算法,便可以進行最佳化配置垃圾收集器。
  
  垃圾收集的目的在於清除不再使用的物件。 gc透過確定物件是否被活動物件引用來決定是否收集該物件。 gc首先要判斷該物件是否是時候可以收集。兩種常用的方法是引用計數和物件引用遍歷。
  
  1.1.引用計數
  
  引用計數存儲對特定對象的所有引用數,也就是說,當應用程序創建引用以及引用超出範圍時,jvm必須適當增減引用數。當某物件的引用數為0時,便可以進行垃圾收集。
  
  1.2.物件引用遍歷
  
  早期的jvm使用參考計數,現在大多數jvm採用物件參考遍歷。物件引用遍歷從一組物件開始,沿著整個物件圖上的每條鏈接,遞歸確定可到達(reachable)的物件。如果某物件無法從這些根物件的一個(至少一個)到達,則將它當作垃圾收集。在物件遍歷階段,gc必須記住哪些物件可以到達,以便刪除不可到達的對象,稱為標記(marking)物件。
  
  下一步,gc要刪除不可到達的物件。刪除時,有些gc只是簡單的掃描堆疊,刪除未標記的未標記的對象,並釋放它們的記憶體以產生新的對象,這叫做清除(sweeping)。這種方法的問題在於記憶體會分成好多小段,而它們不足以用於新的對象,但是組合起來卻很大。因此,許多gc可以重新組織記憶體中的對象,並進行壓縮(compact),形成可利用的空間。
  
  為此,gc需要停止其他的活動活動。這種方法意味著所有與應用程式相關的工作停止,只有gc運行。結果,在回應期間增減了許多混雜請求。另外,更複雜的gc不斷增加或同時運行以減少或清除應用程式的中斷。有的gc使用單線程完成這項工作,有的則採用多線程以增加效率。
  
2.幾種垃圾回收機制
  
  2.1.標記-清除收集器
  
  這種收集器首先遍歷並標記可到達的物件,然後掃描它們以釋放它們的記憶體。這種收集器一般使用單線程工作並停止其他操作。
  
  2.2.標記-壓縮收集器
  
  有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段。在第二階段,則把標記物件複製到堆疊的新域中以便壓縮堆疊。這種收集器也停止其他操作。
  
  2.3.複製收集器
  
  這種收集器將堆疊分為兩個域,常稱為半空間。每次僅使用一半的空間,jvm產生的新物件則放在另一半空間。 gc運作時,它把可到達物件複製到另一半空間,從而壓縮了堆疊。這種方法適用於短生存期的對象,持續複製長生存期的對象則導致效率降低。
  
  2.4.增量收集器
  
  增量收集器把堆疊分為多個域,每次僅從一個域收集垃圾。這會造成較小的應用程式中斷。
  
  2.5.分代收集器
  
  這種收集器把堆疊分為兩個或多個域,用以存放不同壽命的物件。 jvm產生的新物件一般會放在其中的某個網域中。過一段時間,繼續存在的物件將獲得使用期並轉入更長壽命的域中。分代收集器對不同的域使用不同的演算法以優化性能。
  
  2.6.並發收集器
  
  並發收集器與應用程式同時運作。這些收集器在某點上(例如壓縮時)一般都必須停止其他操作以完成特定的任務,但是因為其他應用程式可進行其他的後台操作,所以中斷其他處理的實際時間大大降低。
    2.7.並行收集器
  
  並行收集器使用某種傳統的演算法並使用多執行緒並行的執行它們的工作。在多cpu機器上使用多執行緒技術可以顯著的提高java應用程式的可擴展性。

3.對象的銷毀過程

在对象的销毁过程中,按照对象的finalize的执行情况,可以分为以下几种,系统会记录对象的对应状态:
unfinalized 没有执行finalize,系统也不准备执行。
finalizable 可以执行finalize了,系统会在随后的某个时间执行finalize。
finalized 该对象的finalize已经被执行了。

GC怎么来保持对finalizable的对象的追踪呢。GC有一个Queue,叫做F-Queue,所有对象在变为finalizable的时候会加入到该Queue,然后等待GC执行它的finalize方法。

这时我们引入了对对象的另外一种记录分类,系统可以检查到一个对象属于哪一种。
reachable 从活动的对象引用链可以到达的对象。包括所有线程当前栈的局部变量,所有的静态变量等等。
finalizer-reachable 除了reachable外,从F-Queue可以通过引用到达的对象。
unreachable 其它的对象。

来看看对象的状态转换图。

簡單理解Java的垃圾回收機制與finalize方法的作用

好大,好晕,慢慢看。

1 首先,所有的对象都是从Reachable+Unfinalized走向死亡之路的。

2 当从当前活动集到对象不可达时,对象可以从Reachable状态变到F-Reachable或者Unreachable状态。

3 当对象为非Reachable+Unfinalized时,GC会把它移入F-Queue,状态变为F-Reachable+Finalizable。

4 好了,关键的来了,任何时候,GC都可以从F-Queue中拿到一个Finalizable的对象,标记它为Finalized,然后执行它的finalize方法,由于该对象在这个线程中又可达了,于是该对象变成Reachable了(并且Finalized)。而finalize方法执行时,又有可能把其它的F-Reachable的对象变为一个Reachable的,这个叫做对象再生。

5 当一个对象在Unreachable+Unfinalized时,如果该对象使用的是默认的Object的finalize,或者虽然重写了,但是新的实现什么也不干。为了性能,GC可以把该对象之间变到Reclaimed状态直接销毁,而不用加入到F-Queue等待GC做进一步处理。

6 从状态图看出,不管怎么折腾,任意一个对象的finalize只至多执行一次,一旦对象变为Finalized,就怎么也不会在回到F-Queue去了。当然没有机会再执行finalize了。

7 当对象处于Unreachable+Finalized时,该对象离真正的死亡不远了。GC可以安全的回收该对象的内存了。进入Reclaimed。


对象重生的例子

class C { 
  static A a; 
} 
  
class A { 
  B b; 
  
  public A(B b) { 
    this.b = b; 
  } 
  
  @Override
  public void finalize() { 
    System.out.println("A finalize"); 
    C.a = this; 
  } 
} 
  
class B { 
  String name; 
  int age; 
  
  public B(String name, int age) { 
    this.name = name; 
    this.age = age; 
  } 
  
  @Override
  public void finalize() { 
    System.out.println("B finalize"); 
  } 
  
  @Override
  public String toString() { 
    return name + " is " + age; 
  } 
} 
  
public class Main { 
  public static void main(String[] args) throws Exception { 
    A a = new A(new B("allen", 20)); 
    a = null; 
  
    System.gc(); 
    Thread.sleep(5000); 
    System.out.println(C.a.b); 
  } 
}
登入後複製

期待输出

A finalize 
B finalize 
allen is 20
登入後複製

但是有可能失败,源于GC的不确定性以及时序问题,多跑几次应该可以有成功的。详细解释见文末的参考文档。

    3.1对象的finalize的执行顺序

所有finalizable的对象的finalize的执行是不确定的,既不确定由哪个线程执行,也不确定执行的顺序。 
考虑以下情况就明白为什么了,实例a,b,c是一组相互循环引用的finalizable对象。

    3.2何时及如何使用finalize

从以上的分析得出,以下结论。 
(1) 最重要的,尽量不要用finalize,太复杂了,还是让系统照管比较好。可以定义其它的方法来释放非内存资源。 
(2) 如果用,尽量简单。 
(3) 如果用,避免对象再生,这个是自己给自己找麻烦。 
(4) 可以用来保护非内存资源被释放。即使我们定义了其它的方法来释放非内存资源,但是其它人未必会调用该方法来释放。在finalize里面可以检查一下,如果没有释放就释放好了,晚释放总比不释放好。 
(5) 即使对象的finalize已经运行了,不能保证该对象被销毁。要实现一些保证对象彻底被销毁时的动作,只能依赖于java.lang.ref里面的类和GC交互了。 

更多簡單理解Java的垃圾回收機制與finalize方法的作用相关文章请关注PHP中文网!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!