自動內存管理機制-垃圾收集器與內存分配策略

垃圾收集器與內存分配策略

關於GC的三件事:

那些內存需要回收?

什麼時候回收?

如何回收?

Java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方棧3個區域隨着線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(儘管在運行起會由JIT編譯器進行一些優化,但在本章基於概念模型的討論中,大體上可以認爲是編譯期可知的),因此這幾個內存區域的分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存就自然跟着回收了。java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序運行期間時才知道會創建那些對象,這部分內存的分配和回收是動態的,垃圾回收器所關注的是這部分內存。

 

1、判斷對象生存或者死亡的方法

1.1引用計數法:給對象添加一個引用計數器,每當有一個地方引用過它時,計數器的值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的。在java虛擬機中不使用引用計數法的原因是它很難解決對象之間相互循環引用的問題。

1.2可達性分析算法(javaC#Lisp):該算法的基本思想就是通過乙烯類成爲GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象不可達。如下圖所示,對象object5 object6 object7雖然相互有關聯,但是它們的GC Roots是不可達的,所以他們將會被回收。

 

java語言中,可作爲GC Roots的對象包括下面幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象;

方法區中類靜態屬性引用的對象;

方法區中常量引用的對象;

本地方法棧JNI(即一般說的Native方法)引用對象。

1.3再談引用:在JDK1.2之後,java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用4種,這4種引用強度依次逐漸減弱。

強引用:強引用就是指在程序代碼之中普遍存在的,類似於Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用:軟引用是用來描述一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。

弱引用:弱引用是用來描述非必須對象的,但是它的強度比軟引用關聯的對象只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用:虛引用也稱爲幽靈引用或者幻影引用,它是最弱的關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

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

1.5回收方法區(永久代):永久帶的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

廢棄常量:回收廢棄常量與java堆中的對象非常相似。以常量池中的字面量的回收爲例,假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話,這個“abc”常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

無用的類:一個類需要同時滿足下面3個條件才能算是“無用的類”:(1)該類的所有實例都已經被回收,也就是java堆中不存在改類的任何實例。(2)加載該類的ClassLoader已經被回收。(3)改類對應的java.lang.Class對象沒有在任何地方引用,無法在任何地方通過反射來訪問該類的方法。虛擬機可以對滿足上述3個條件的無用類進行回收,這裏說的僅僅是“可以”,而並不和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnclassgc參數進行控制。在大量使用反射、動態代理、CGLibByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

 

2、垃圾收集算法

2.1標記-清除算法:最基礎的收集算法是“標記-清除”算法,如同它的名字一樣,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它有兩個不足,一是效率太低,標記和清除兩個過程;另一個是空間問題問題,標記清除之後會產生大量不連續的內存碎片,內存碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前出發另一次垃圾收集動作

2.2複製算法:複製算法的出現是爲了解決效率問題,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這塊內存用完了,就將還存活着的對象複製到另外一塊上面,然後把已經使用過的內存空間一次清理掉。這樣使得每次都是對陣個半區進行內存回收,內存分配時也就不用考慮內存碎片的等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。

2.3標記-整理算法:讓所有存活的對象都向一端移動,然後直接清理掉端便捷以外的內存。

2.4分帶收集算法:分代手機算法根據對象存活週期的不同將內存劃分爲幾塊。一般是把java堆分爲新生代和老生代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清除”或“標記-整理”算法來進行回收。

 

3HotSpot的算法實現

上面介紹了對象存活判定算法和垃圾收集算法,而在HotSpot虛擬機上實現這些算法時,必須對算法執行嚴格有效的考量,才能保證虛擬機高效的運行。

3.1枚舉根節點:從可達性分析算法中從GC Roots節點找到引用鏈這個操作爲例,可作爲GC Roots的節點主要在全局的引用(例如常量或類靜態屬性分)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裏面的引用,那麼必然會消耗很多時間,因此HotSpot虛擬機實現,是使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據結構計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中那些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。還有就是可達性分析算法對執行時間的敏感還體現在GC停頓上,因爲這項分析工作必須在一個能確保一致性的快照中進行-----這裏“一致性”的意思是指在整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用還在不斷變化的情況,改點不滿足的話分析結果準確性就無法得到保證。

3.2安全點:前面已經提到,只是在“特定的位置”記錄了這些信息,這些位置稱爲安全點,即程序執行時並非在所有的地方都能停頓下來開始GC,只有在到達安全點才能暫停。對於安全點,另外一個需要考慮的問題是如何在GC發生時讓所有線程都(這裏不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來。這裏有兩種方案可供選擇:“搶先中斷式”和“主動中斷”。

3.3安全區域:安全區域是指在一段代碼段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。

 

4、垃圾收集器

垃圾收集算法是內存回收的方法論,而垃圾收集器就是內存回收的具體實現。

4.1Serial(串行)收集器:串行收集器是一個單線程收集器,但它的“單線程”的意義並不僅僅是說明它只會使用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是它在進行垃圾回收時必須暫停其他所有的工作線程,直到它結束。Client端用的比較多的新生代收集器。

4.2ParNew(並行新生代)收集器:ParNew收集器其實就是Serial收集器的多線程版本。主要運行在Server模式下的新生代收集器。

並行:指多條垃圾收集線程同時執行工作,但此時用戶線程仍然處於等待狀態。

併發:指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶線程在繼續執行,而垃圾收集程序運行於另外一個CPU上。

4.3Parallel Scavenge(並行清除)收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂的吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。

4.4Serial Old收集器:Serial Oldserial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在Client模式下的虛擬機使用。

4.5Parallel Old收集器:Parallel OldParallel Scanenge收集器的老年代版本,使用多線程和“標記-整理”算法。

4.6CMS收集器:CMS收集器是一種以獲取最短回收停頓 時間爲目標的收集器。CMS收集器是基於“標記-清除”算法實現的,它分爲4個步驟:初始標記,併發標記,重新標記,併發清除。

4.7G1收集器

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章