1. 程式人生 > >Java回收機制總結

Java回收機制總結

all sleep 內存區域 操作 比例 服務 釋放內存 永生 itl

Java回收機制

如何確定“垃圾”

既然是垃圾回收機制,第一步肯定是要確定垃圾,知道了垃圾便可以進行回收。但是如何確定垃圾呢?什麽是垃圾呢?

什麽是“垃圾”

首先要明白什麽是“垃圾”,垃圾回收機制是回收堆內存中的對象(具體的內存劃分可以看:),對於棧中的對象是不需要回收機制去考慮的。在Java中堆內存中的對象是通過和棧內存中的引用相互關聯,才被利用的。既然是對堆內存的回收,並且堆內存中存儲的都是引用對象的實體,所以回收的就是沒有被任何一個引用所關聯的實體對象。

因此,“垃圾”實質上指的是java虛擬機中堆內存裏沒有被引用到的,永遠也不會訪問到的實體對象。

1. 引用計數法

既然明白了什麽是“垃圾”,那麽該如何找垃圾呢?很顯然,通過定義可知,沒有引用的對象就是垃圾,所以可以通過查看對象的當前被引用的數量來判斷是否應該回收。

比如當一個引用關聯了實體對象後,就在對象的引用數加1,若取消引用則減1.當引用數為0時即被系統回收。看似簡單的方法,卻十分高效,但是卻存在一個弊端——循環引用。假如兩個對象互相引用時,各自的引用數都為1,可是對象是永遠都訪問不到的,如下代碼:

public class Main {

public static void main(String[] args) {

MyObject object1 = new MyObject();

MyObject object2 = new MyObject();

object1.object = object2;

object2.object = object1;

object1 = null;

object2 = null;

}

}

class MyObject{

public Object object = null;

}

最後面兩句將object1和object2賦值為null,也就是說object1和object2指向的對象已經不可能再被訪問,但是由於計數不為0(因為object1中的object和object2中的object指向對方),導致對象始終不會被回收。

技術分享圖片

所以在JAVA中並沒有采用這種方法。(Python采用的是引用計數法)

2. 可達性分析算法

可以作為GC Root 引用點的是:

  JavaStack中的引用的對象。

  方法區中靜態引用指向的對象。

  方法區中常量引用指向的對象。

  Native方法中JNI引用的對象。

GC管理的主要區域是Java堆,一般情況下只針對堆進行垃圾回收。方法區、棧和本地方法區不被GC所管理,因而選擇這些區域內的對象作為GC roots,被GC roots引用的對象不被GC回收。

目前主流的編程語言(java,C#等)的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。

技術分享圖片

.GC判斷為垃圾的對象一定會回收嗎?

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”(沒有必要執行finalize方法)。(即意味著直接回收)

如果這個對象被判定為有必要執行finalize()方法,那麽這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。

finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

代碼示例:

  1. public class FinalizeEscapeGC {
  2. public static FinalizeEscapeGC SAVE_HOOK = null;
  3. public void isAlive() {
  4. System.out.println("yes,i am still alive:)");
  5. }
  6. @Override
  7. protected void finalize() throws Throwable {
  8. super.finalize();
  9. System.out.println("finalize mehtod executed!");
  10. FinalizeEscapeGC.SAVE_HOOK = this;
  11. }

14.

  1. public static void main(String[] args) throws Throwable {
  2. SAVE_HOOK = new FinalizeEscapeGC();
  3. // 對象第一次成功拯救自己
  4. SAVE_HOOK = null;
  5. System.gc();
  6. // 因為finalize方法優先級很低,所以暫停0.5秒以等待它
  7. Thread.sleep(500);
  8. if (SAVE_HOOK != null) {
  9. SAVE_HOOK.isAlive();
  10. } else {
  11. System.out.println("no,i am dead:(");
  12. }
  13. // 下面這段代碼與上面的完全相同,但是這次自救卻失敗了
  14. SAVE_HOOK = null;
  15. System.gc();
  16. // 因為finalize方法優先級很低,所以暫停0.5秒以等待它
  17. Thread.sleep(500);
  18. if (SAVE_HOOK != null) {
  19. SAVE_HOOK.isAlive();
  20. } else {
  21. System.out.println("no,i am dead:(");
  22. }
  23. }

38.}

39.

40.運行結果:

41.

42.finalize mehtod executed!

43.yes,i am still alive:

44.no,i am dead:

SAVE_HOOK對象的finalize()方法確實被GC收集器觸發過,並且在被收集前成功逃脫了。另外一個值得註意的地方是,代碼中有兩段完全一樣的代碼片段,執行結果卻是一次逃脫成功,一次失敗,這是因為任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了。因為finalize()方法已經被虛擬機調用過,虛擬機都視為“沒有必要執行”。(即意味著直接回收)

finalize()方法

大致描述一下finalize流程:當對象變成(GC Roots)不可達時,GC會判斷該對象是否覆蓋(重寫)了finalize方法,若未覆蓋,則直接將其回收。否則(即覆蓋了的話),並且對象的finalize方法未被執行過(如果已經被執行過了就直接被GC回收了,就像上面的例子),則將其放入F-Queue隊列,由一個低優先級線程執行該隊列中對象的finalize方法。執行finalize方法完畢後,GC會再次判斷該對象是否可達,若不可達,則進行回收,否則,對象“復活”。

垃圾回收器準備釋放內存的時候,會先調用finalize()。

(1).對象不一定會被回收。

(2).垃圾回收不是析構函數。

(3).垃圾回收只與內存有關。

(4).垃圾回收和finalize()都是靠不住的,只要JVM還沒有快到耗盡內存的地步,它是不會浪費時間進行垃圾回收

之所以要使用finalize(),是存在著垃圾回收器不能處理的特殊情況。假定你的對象(並非使用new方法)獲得了一塊“特殊”的內存區域,由於垃圾回收器只知道那些顯示地經由new分配的內存空間,所以它不知道該如何釋放這塊“特殊”的內存區域,那麽這個時候Java允許在類中定義一個finalize()方法。

特殊的區域例如:1)由於在分配內存的時候可能采用了類似C語言的做法,而非JAVA的通常new做法。這種情況主要發生在native method中,比如native method調用了C/C++方法malloc()函數系列來分配存儲空間,但是除非調用free()函數,否則這些內存空間將不會得到釋放,那麽這個時候就可能造成內存泄漏。但是由於free()方法是在C/C++中的函數,所以finalize()中可以用本地方法來調用它。以釋放這些“特殊”的內存空間。2)又或者打開的文件資源,這些資源不屬於垃圾回收器的回收範圍。

換言之,finalize()的主要用途是釋放一些其他做法開辟的內存空間,以及做一些清理工作。因為在JAVA中並沒有提夠像“析構”函數或者類似概念的函數,要做一些類似清理工作的時候,必須自己動手創建一個執行清理工作的普通方法,也就是override Object這個類中的finalize()方法。例如,假設某一個對象在創建過程中會將自己繪制到屏幕上,如果不是明確地從屏幕上將其擦出,它可能永遠都不會被清理。如果在finalize()加入某一種擦除功能,當GC工作時,finalize()得到了調用,圖像就會被擦除。要是GC沒有發生,那麽這個圖像就會被一直保存下來。

一旦垃圾回收器準備好釋放對象占用的存儲空間,首先會去調用finalize()方法進行一些必要的清理工作。只有到下一次再進行垃圾回收動作的時候,才會真正釋放這個對象所占用的內存空間。

在普通的清除工作中,為清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這與C++"析構函數"的概念稍有抵觸。在C++中,所有對象都會破壞(清除)。或者換句話說,所有對象都"應該"破壞。若將C++對象創建成一個本地對象,比如在堆棧中創建(在Java中是不可能的,Java都在堆中),那麽清除或破壞工作就會在"結束花括號"所代表的、創建這個對象的作用域的末尾進行。若對象是用new創建的(類似於Java),那麽當程序員調用C++的 delete命令時(Java沒有這個命令),就會調用相應的析構函數。若程序員忘記了,那麽永遠不會調用析構函數,我們最終得到的將是一個內存"漏洞",另外還包括對象的其他部分永遠不會得到清除。
  相反,Java不允許我們創建本地(局部)對象--無論如何都要使用new。但在Java中,沒有"delete"命令來釋放對象,因為垃圾回收器會幫助我們自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾回收機制,所以Java沒有析構函數。然而,隨著以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對析構函數的需要,或者說不能消除對析構函數代表的那種機制的需要(原因見下一段。另外finalize()函數是在垃圾回收器準備釋放對象占用的存儲空間的時候被調用的,絕對不能直接調用finalize()所以應盡量避免用它)。若希望執行除釋放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java中的一個方法。它等價於C++的析構函數,只是沒後者方便。

在C++中所有的對象運用delete()一定會被銷毀,而JAVA裏的對象並非總會被垃圾回收器回收。In another word, 1 對象可能不被垃圾回收,2 垃圾回收並不等於“析構”,3 垃圾回收只與內存有關。也就是說,並不是如果一個對象不再被使用,是不是要在finalize()中釋放這個對象中含有的其它對象呢?不是的。因為無論對象是如何創建的,垃圾回收器都會負責釋放那些對象占有的內存。

典型的垃圾回收算法

1. 標記-清除算法(Mark-Sweep)

最基礎的垃圾回收算法,分為兩個階段,標註和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所占用的空間。如圖:

技術分享圖片

從圖中我們就可以發現,該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可利用空間的問題。

2. 復制算法(Copying)

為了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分為等大小的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象復制到另一塊上去,把已使用的內存清掉,如圖:

技術分享圖片

這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原本的一半。且存活對象增多的話,Copying算法的效率會大大降低。

3. 標記-整理算法(Mark-Compact)

結合了以上兩個算法,為了避免缺陷而提出。標記階段和Mark-Sweep算法相同,標記後不是清理對象,而是將存活對象移向內存的一端。然後清除端邊界外的對象。如圖:

技術分享圖片

2.4. 分代收集算法(Generational Collection)

分代收集法是目前大部分JVM所采用的方法,其核心思想是根據對象存活的不同生命周期將內存劃分為不同的域,一般情況下將GC堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。

目前大部分JVM的GC對於新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,即要復制的操作比較少,但通常並不是按照1:1來劃分新生代。一般將新生代劃分為一塊較大的Eden空間和兩個較小的Survivor空間(From Space, To Space),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將該兩塊空間中還存活的對象復制到另一塊Survivor空間中。

技術分享圖片

而老生代因為每次只回收少量對象,因而采用Mark-Compact算法。

另外,不要忘記在Java基礎:Java虛擬機(JVM)中提到過的處於方法區的永生代(Permanet Generation)。它用來存儲class類,常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。

對象的內存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放對象的那一塊),少數情況會直接分配到老生代。當新生代的Eden Space和From Space空間不足時就會發生一次GC,進行GC後,Eden Space和From Space區的存活對象會被挪到To Space,然後將Eden Space和From Space進行清理。如果To Space無法足夠存儲某個對象,則將這個對象存儲到老生代。在進行GC後,使用的便是Eden Space和To Space了(因為此時From Space被清空了,所以它和To Space 角色互換了),如此反復循環。當對象在Survivor區躲過一次GC後,其年齡就會+1。默認情況下年齡到達15的對象會被移到老生代中。

下面是對分代算法的詳細描述:

技術分享圖片

分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的回收算法,以便提高回收效率。

年輕代(Young Generation)

1.所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。

2.新生代內存按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象復制到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象復制到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。

3.當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收

4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)

年老代(Old Generation

1.在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。

2.內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。

持久代(Permanent Generation)

用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。

典型的垃圾收集器

垃圾收集算法是垃圾收集器的理論基礎,而垃圾收集器就是其具體實現。下面介紹HotSpot虛擬機提供的幾種垃圾收集器。

3.1. Serial/Serial Old

最古老的收集器,是一個單線程收集器,用它進行垃圾回收時,必須暫停所有用戶線程。Serial是針對新生代的收集器,采用Copying算法;而Serial Old是針對老生代的收集器,采用Mark-Compact算法。優點是簡單高效,缺點是需要暫停用戶線程。

3.2. ParNew

Seral/Serial Old的多線程版本,使用多個線程進行垃圾收集。

3.3. Parallel Scavenge

新生代的並行收集器,回收期間不需要暫停其他線程,采用Copying算法。該收集器與前兩個收集器不同,主要為了達到一個可控的吞吐量。

3.4. Parallel Old

Parallel Scavenge的老生代版本,采用Mark-Compact算法和多線程。

3.5. CMS

Current Mark Sweep收集器是一種以最小回收時間停頓為目標的並發回收器,因而采用Mark-Sweep算法。

3.6. G1

G1(Garbage First)收集器技術的前沿成果,是面向服務端的收集器,能充分利用CPU和多核環境。是一款並行與並發收集器,它能夠建立可預測的停頓時間模型。

本文是一篇總結性的文章,借鑒了下面網站的內容

https://blog.csdn.net/mine_song/article/details/63251367

https://www.cnblogs.com/andy-zcx/p/5522836.html

https://www.cnblogs.com/cielosun/p/6674431.html

https://www.i3geek.com/archives/1220

Java回收機制總結