1. 程式人生 > 其它 >Java效能調優基礎之垃圾回收:回收哪些記憶體/物件 引用計數演算法 可達性分析演算法 finalize()方法 HotSpot實現分析

Java效能調優基礎之垃圾回收:回收哪些記憶體/物件 引用計數演算法 可達性分析演算法 finalize()方法 HotSpot實現分析

Java虛擬機器垃圾回收

   垃圾回收,或稱垃圾收集(Garbage Collection,GC)是指自動管理回收不再被引用的記憶體資料。

   在1960年誕生於MIT的Lisp語言首次使用了動態記憶體分配和垃圾收集技術,可以實現垃圾回收的一個基本要求是語言是型別安全的,現在使用的包括Java、Perl、ML等。

為什麼需要了解垃圾回收

   目前記憶體的動態分配與記憶體回收技術已經相當成熟,但為什麼還需要去了解記憶體分配與GC呢?

   1、當需要排查各種記憶體溢位、記憶體洩漏問題時;

   2、當垃圾收整合為系統達到更高併發量的瓶頸時;

   我們就需要對這些"自動化"技術實話必要的監控和調節;

垃圾回收需要了解什麼

   思考GC完成的3件事:

   1、哪些記憶體需要回收?即如何判斷物件已經死亡;

   2、什麼時候回收?即GC發生在什麼時候?需要了解GC策略,與垃圾回收器實現有關;

   3、如何回收?即需要了解垃圾回收演算法,及演算法的實現--垃圾回收器;

  第一點就是本文下面的主題,這是垃圾回收的基礎,如:可達性分析演算法是後面垃圾回收演算法的基礎,而判斷哪些物件可以回收是垃圾回收的首要任務。

判斷物件可以回收

   垃圾收集器對堆進行回收前,首先要確定堆中的物件哪些還"存活",哪些已經"死去";

   下面先來了解兩種判斷物件不再被引用的演算法,再來談談物件的引用,最後來看如何真正宣告一個物件死亡。

    ### 引用計數演算法(Recference Counting)
    1、演算法基本思路

     給物件新增一個引用計數器,每當有一個地方引用它,計數器加1;

     當引用失效,計數器值減1;

     任何時刻計數器值為0,則認為物件是不再被使用的;    

    2、優點

      實現簡單,判定高效,可以很好解決大部分場景的問題,也有一些著名的應用案例;

    3、缺點

  (A)、很難解決物件之間相互迴圈引用的問題
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;
    當兩個物件不再被訪問時,因為相互引用對方,導致引用計數不為0;
    更復雜的迴圈資料結構,如圖(《編譯原理》7-18):

  (B)、並且開銷較大,頻繁且大量的引用變化,帶來大量的額外運算;

    主流的JVM都沒有選用引用計數演算法來管理記憶體;

   可達性分析演算法(Reachability Analysis)也稱為傳遞跟蹤演算法;

   主流的呼叫程式語言(Java、C#等)在主流的實現中,都是通過可達性分析來判定物件是否存活的。

    1、演算法基本思路

    通過一系列"GC Roots"物件作為起始點,開始向下搜尋;

    搜尋所走過和路徑稱為引用鏈(Reference Chain);

    當一個物件到GC Roots沒有任何引用鏈相連時(從GC Roots到這個物件不可達),則證明該物件是不可用的;

    2、GC Roots物件

    Java中,GC Roots物件包括:

  (1)、虛擬機器棧(棧幀中本地變量表)中引用的物件;

  (2)、方法區中類靜態屬性引用的物件;

  (3)、方法區中常量引用的物件;

  (4)、本地方法棧中JNI(Native方法)引用的物件;

   主要在執行上下文中和全域性性的引用;

    3、優點

    更加精確和嚴謹,可以分析出迴圈資料結構相互引用的情況;

    4、缺點

    實現比較複雜;

    需要分析大量資料,消耗大量時間;

    分析過程需要GC停頓(引用關係不能發生變化),即停頓所有Java執行執行緒(稱為"Stop The World",是垃圾回收重點關注的問題);

    後面會針對HotSpot虛擬機器實現的可達性分析演算法進行介紹,看看是它如何解決這些缺點的。

    再談物件引用
    在《Java物件在Java虛擬機器中的引用訪問方式》曾詳細介紹過物件的引用問題,這與物件回收演算法有很大關係,下面再來了解下。

    java程式通過reference型別資料操作堆上的具體物件;

    1、JVM層面的引用

    reference型別是引用型別(Reference Types)的一種;

    JVM規範規定reference型別來表示對某個物件的引用,可以想象成類似於一個指向物件的指標;

    物件的操作、傳遞和檢查都通過引用它的reference型別的資料進行操作;

    2、Java語言層面的引用

   (i)、JDK1.2前的引用定義

    如果reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用;

    這種定義太過狹隘,無法描述更多資訊;

  (ii)、JDK1.2後,對引用概念進行了擴充,將引用分為:

  (1)、強引用(Strong Reference)

    程式程式碼普遍存在的,類似"Object obj=new Object()";

    只要強引用還存在,GC永遠不會回收被引用的物件;

  (2)、軟引用(Soft Reference)

    用來描述還有用但並非必需的物件;

    直到記憶體空間不夠時(丟擲OutOfMemoryError之前),才會被垃圾回收;

    最常用於實現對記憶體敏感的快取;

    SoftReference類實現;

  (3)、弱引用(Weak Reference)

    用來描述非必需物件;

    只能生存到下一次垃圾回收之前,無論記憶體是否足夠;

    WeakReference類實現;

  (4)、虛引用(Phantom Reference)

    也稱為幽靈引用或幻影引用;

    完全不會對其生存時間構成影響;

    唯一目的就是能在這個物件被回收時收到一個系統通知;

    PhantomRenference類實現;                

    更多請參考JDK相關API說明;

    對於軟引用,可以使用命令列選項"-XX:SoftRefLRUPolicyMSPerMB = <N>"來控制清除速率;

    [更多請參考](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref65)

    判斷物件生存還是死亡
    要真正宣告一個物件死亡,至少要經歷兩次標記過程。

    1、第一次標記

    在可達性分析後發現到GC Roots沒有任何引用鏈相連時,被第一次標記;

    並且進行一次篩選:此物件是否必要執行finalize()方法;

  (A)、沒有必要執行

    沒有必要執行的情況:

  (1)、物件沒有覆蓋finalize()方法;

  (2)、finalize()方法已經被JVM呼叫過;

  這兩種情況就可以認為物件已死,可以回收;

  (B)、有必要執行

    對有必要執行finalize()方法的物件,被放入F-Queue佇列中;

    稍後在JVM自動建立、低優先順序的Finalizer執行緒(可能多個執行緒)中觸發這個方法;

    2、第二次標記

    GC將對F-Queue佇列中的物件進行第二次小規模標記;

    finalize()方法是物件逃脫死亡的最後一次機會:
  (A)、如果物件在其finalize()方法中重新與引用鏈上任何一個物件建立關聯,第二次標記時會將其移出"即將回收"的集合;

  (B)、如果物件沒有,也可以認為物件已死,可以回收了;                    

  一個物件的finalize()方法只會被系統自動呼叫一次,經過finalize()方法逃脫死亡的物件,第二次不會再呼叫;

  finalize()方法
  上面已經說到finalize()方法與垃圾回收第二次標記相關,下面瞭解下在Java語言層面有哪些需要注意的。

  finalize()是Object類的一個方法,是Java剛誕生時為了使C/C++程式設計師容易接受它所做出的一個妥協,但不要當作類似C/C++的解構函式;

  因為它執行的時間不確定,甚至是否被執行也不確定(Java程式的不正常退出),而且執行代價高昂,無法保證各個物件的呼叫順序(甚至有不同執行緒中呼叫);

  如果需要"釋放資源",可以定義顯式的終止方法,並在"try-catch-finally"的finally{}塊中保證及時呼叫,如File相關類的close()方法;

  此外,finalize()方法主要有兩種用途:

  1、充當"安全網"

  當顯式的終止方法沒有呼叫時,在finalize()方法中發現後發出警告;

  但要考慮是否值得付出這樣的代價;

  如FileInputStream、FileOutputStream、Timer和Connection類中都有這種應用;

  2、與物件的本地對等體有關

  本地對等體:普通物件呼叫本地方法(JNI)委託的本地物件;

  本地對等體不會被GC回收;

  如果本地對等體不擁有關鍵資源,finalize()方法裡可以回收它(如C/C++中malloc(),需要呼叫free());

  如果有關鍵資源,必須顯式的終止方法;

  一般情況下,應儘量避免使用它,甚至可以忘掉它。

HotSpot虛擬機器中物件可達性分析的實現

前面對可達性分析演算法進行介紹,並看到了它在判斷物件存活與死亡的作用,下面看看是HotSpot虛擬機器是如何實現可達性分析演算法,如何解決相關缺點的。

可達性分析的問題

  1、消耗大量時間

  從前面可達性分析知道,GC Roots主要在全域性性的引用(常量或靜態屬性)和執行上下文中(棧幀中的本地變量表);

  要在這些大量的資料中,逐個檢查引用,會消耗很多時間;

  2、GC停頓

  可達性分析期間需要保證整個執行系統的一致性,物件的引用關係不能發生變化;

  導致GC進行時必須停頓所有Java執行執行緒(稱為"Stop The World");

  (幾乎不會發生停頓的CMS收集器中,列舉根節點時也是必須要停頓的)

  Stop The World:

  是JVM在後臺自動發起和自動完成的;

  在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉;

  列舉根節點
  列舉根節點也就是查詢GC Roots;

  目前主流JVM都是準確式GC,可以直接得知哪些地方存放著物件引用,所以執行系統停頓下來後,並不需要全部、逐個檢查完全域性性的和執行上下文中的引用位置;

  在HotSpot中,是使用一組稱為OopMap的資料結構來達到這個目的的;

  在類載入時,計算物件內什麼偏移量上是什麼型別的資料;

  在JIT編譯時,也會記錄棧和暫存器中的哪些位置是引用;

  這樣GC掃描時就可以直接得知這些資訊;

  安全點
  1、安全點是什麼,為什麼需要安全點

  HotSpot在OopMap的幫助下可以快速且準確的完成GC Roots列舉,但是這有一個問題:        

  執行中,非常多的指令都會導致引用關係變化;

  如果為這些指令都生成對應的OopMap,需要的空間成本太高;

  問題解決:

  只在特定的位置記錄OopMap引用關係,這些位置稱為安全點(Safepoint);

  即程式執行時並非所有地方都能停頓下來開始GC;

  2、安全點的選定

  不能太少,否則GC等待時間太長;也不能太多,否則GC過於頻繁,增大執行時負荷;

  所以,基本上是以程式"是否具有讓程式長時間執行的特徵"為標準選定;

  "長時間執行"最明顯的特徵就是指令序列複用,如:方法呼叫、迴圈跳轉、迴圈的末尾、異常跳轉等;

  只有具有這些功能的指令才會產生Safepoint;

  3、如何在安全點上停頓

  對於Safepoint,如何在GC發生時讓所有執行緒(不包括JNI執行緒)執行到其所在最近的Safepoint上再停頓下來?

  主要有兩種方案可選:

 (A)、搶先式中斷(Preemptive Suspension)

  不需要執行緒主動配合,實現如下:

  (1)、在GC發生時,首先中斷所有執行緒;

  (2)、如果發現不在Safepoint上的執行緒,就恢復讓其執行到Safepoint上;

  現在幾乎沒有JVM實現採用這種方式;

(B)、主動式中斷(Voluntary Suspension)

  (1)、在GC發生時,不直接操作執行緒中斷,而是僅簡單設定一個標誌;

  (2)、讓各執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起;

  而輪詢標誌的地方和Safepoint是重合的;    

  在JIT執行方式下:test指令是HotSpot生成的輪詢指令;

  一條test彙編指令便完成Safepoint輪詢和觸發執行緒中斷;

  安全區域
  1、為什麼需要安全區域

  對於上面的Safepoint還有一個問題:

  程式不執行時沒有CPU時間(Sleep或Blocked狀態),無法執行到Safepoint上再中斷掛起;    

  這就需要安全區域來解決;

  2、什麼是安全區域(Safe Region)

  指一段程式碼片段中,引用關係不會發生變化;

  在這個區域中的任意地方開始GC都是安全的;

  3、如何用安全區域解決問題

  安全區域解決問題的思路:

  (1)、執行緒執行進入Safe Region,首先標識自己已經進入Safe Region;

  (2)、執行緒被喚醒離開Safe Region時,其需要檢查系統是否已經完成根節點列舉(或整個GC);

  如果已經完成,就繼續執行;

  否則必須等待,直到收到可以安全離開Safe Region的訊號通知;    

  這樣就不會影響標記結果;

  雖然HotSpot虛擬機器中採用了這些方法來解決物件可達性分析的問題,但只是大大減少了這些問題影響,並不能完全解決,如GC停頓"Stop The World"是垃圾回收重點關注的問題,後面介紹垃圾回收器時應注意:低GC停頓是其一個關注。