Java效能調優基礎之垃圾回收:回收哪些記憶體/物件 引用計數演算法 可達性分析演算法 finalize()方法 HotSpot實現分析
阿新 • • 發佈:2021-10-22
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停頓是其一個關注。