摘要:垃圾回收算法與垃圾回收器綜述我們常說的垃圾回收算法可以分為兩部分對象的查找算法與真正的回收方法。串行垃圾回收器一次只使用一個線程進行垃圾回收并行垃圾回收器一次將開啟多個線程同時進行垃圾回收。
垃圾回收算法與 JVM 垃圾回收器綜述垃圾回收算法與 JVM 垃圾回收器綜述歸納于筆者的 JVM 內部原理與性能調優系列文章,文中涉及的引用資料參考 Java 學習與實踐資料索引、JVM 資料索引。
我們常說的垃圾回收算法可以分為兩部分:對象的查找算法與真正的回收方法。不同回收器的實現細節各有不同,但總的來說基本所有的回收器都會關注如下兩個方面:找出所有的存活對象以及清理掉所有的其它對象——也就是那些被認為是廢棄或無用的對象。Java 虛擬機規范中對垃圾收集器應該如何實現并沒有任何規定,因此不同的廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大差別,并且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。其中最主流的四個垃圾回收器分別是:通常用于單 CPU 環境的 Serial GC、Throughput/Parallel GC、CMS GC、G1 GC。
當我們在討論垃圾回收器時,往往也會涉及到很多的概念;譬如并行(Parallel)與并發(Concurrent)、Minor GC 與 Major / Full GC。并行指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態;并發指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行于另一個CPU上。Minor GC 指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快;Major GC 指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程),Major GC的速度一般會比Minor GC慢10倍以上。從不同角度分析垃圾回收器,可以將其分為不同的類型:
分類標準 | 描述 |
---|---|
線程數 | 分為串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一個線程進行垃圾回收;并行垃圾回收器一次將開啟多個線程同時進行垃圾回收。在并行能力較強的 CPU 上,使用并行垃圾回收器可以縮短 GC 的停頓時間。 |
工作模式 | 分為并發式垃圾回收器和獨占式垃圾回收器。并發式垃圾回收器與應用程序線程交替工作,以盡可能減少應用程序的停頓時間;獨占式垃圾回收器 (Stop the world) 一旦運行,就停止應用程序中的其他所有線程,直到垃圾回收過程完全結束。 |
碎片處理方式 | 分為壓縮式垃圾回收器和非壓縮式垃圾回收器。壓縮式垃圾回收器會在回收完成后,對存活對象進行壓縮整理,消除回收后的碎片;非壓縮式的垃圾回收器不進行這步操作。 |
工作的內存區間 | 新生代垃圾回收器和老年代垃圾回收器 |
我們最常用的評價垃圾回收器的指標就是吞吐量與停頓時間,停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶的體驗;而高吞吐量則可以最高效率地利用 CPU 時間,盡快地完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務;具體的指標列舉如下:
吞吐量:指在應用程序的生命周期內,應用程序所花費的時間和系統總運行時間的比值。系統總運行時間=應用程序耗時+GC 耗時。如果系統運行了 100min,GC 耗時 1min,那么系統的吞吐量就是 (100-1)/100=99%。
垃圾回收器負載:和吞吐量相反,垃圾回收器負載指來記回收器耗時與系統運行總時間的比值。
停頓時間:指垃圾回收器正在運行時,應用程序的暫停時間。對于獨占回收器而言,停頓時間可能會比較長。使用并發的回收器時,由于垃圾回收器和應用程序交替運行,程序的停頓時間會變短,但是,由于其效率很可能不如獨占垃圾回收器,故系統的吞吐量可能會較低。
垃圾回收頻率:指垃圾回收器多長時間會運行一次。一般來說,對于固定的應用而言,垃圾回收器的頻率應該是越低越好。通常增大堆空間可以有效降低垃圾回收發生的頻率,但是可能會增加回收產生的停頓時間。
反應時間:指當一個對象被稱為垃圾后多長時間內,它所占據的內存空間會被釋放。
堆分配:不同的垃圾回收器對堆內存的分配方式可能是不同的。一個良好的垃圾回收器應該有一個合理的堆內存區間劃分。
在對象查找算法的幫助下我們可以找到內存可以被使用的,或者說那些內存是可以回收,更多的時候我們肯定愿意做更少的事情達到同樣的目的。
對象引用在 JDK 1.2 以前的版本中,若一個對象不被任何變量引用,那么程序就無法再使用這個對象。也就是說,只有對象處于可觸及(Reachable)狀態,程序才能使用它。從 JDK 1.2 版本開始,把對象的引用分為 4 種級別,從而使程序能更加靈活地控制對象的生命周期。這 4 種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
StrongReference: 強引用強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧愿拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。比如下面這段代碼:
public class Main { public static void main(String[] args) { new Main().fun1(); } public void fun1() { Object object = new Object(); Object[] objArr = new Object[1000]; } }
當運行至 Object[] objArr = new Object[1000]; 這句時,如果內存不足,JVM 會拋出 OOM 錯誤也不會回收 object 指向的對象。不過要注意的是,當 fun1 運行完之后,object 和 objArr 都已經不存在了,所以它們指向的對象都會被 JVM 回收。如果想中斷強引用和某個對象之間的關聯,可以顯示地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象。比如 Vector 類的 clear 方法中就是通過將引用賦值為 null 來實現清理工作的:
/** * Removes the element at the specified position in this Vector. * Shifts any subsequent elements to the left (subtracts one from their * indices). Returns the element that was removed from the Vector. * * @throws ArrayIndexOutOfBoundsException if the index is out of range * ({@code index < 0 || index >= size()}) * @param index the index of the element to be removed * @return element that was removed * @since 1.2 */ public synchronized E remove(int index) { modCount++; if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); Object oldValue = elementData[index]; int numMoved = elementCount - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--elementCount] = null; // Let gc do its work return (E)oldValue; }SoftReference: 軟引用
軟引用是用來描述一些有用但并不是必需的對象,在 Java 中用 java.lang.ref.SoftReference 類來表示。對于軟引用關聯著的對象,只有在內存不足的時候 JVM 才會回收該對象。因此,這一點可以很好地用來解決 OOM 的問題,并且這個特性很適合用來實現緩存:比如網頁緩存、圖片緩存等。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關聯的引用隊列中。下面是一個使用示例:
import java.lang.ref.SoftReference; public class Main { public static void main(String[] args) { SoftReferenceWeakReference: 弱引用sr = new SoftReference (new String("hello")); System.out.println(sr.get()); } }
弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { WeakReferencesr = new WeakReference (new String("hello")); System.out.println(sr.get()); System.gc(); //通知JVM的gc進行垃圾回收 System.out.println(sr.get()); } }
輸出結果為:
hello null
第二個輸出結果是 null,這說明只要 JVM 進行垃圾回收,被弱引用關聯的對象必定會被回收掉。不過要注意的是,這里所說的被弱引用關聯的對象是指只有弱引用與之關聯,如果存在強引用同時與之關聯,則進行垃圾回收時也不會回收該對象(軟引用也是如此)。弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
PhantomReference: 虛引用“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用和前面的軟引用、弱引用不同,它并不影響對象的生命周期。在 Java 中用 java.lang.ref.PhantomReference 類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class Main { public static void main(String[] args) { ReferenceQueue對象存活性判斷queue = new ReferenceQueue (); PhantomReference pr = new PhantomReference (new String("hello"), queue); System.out.println(pr.get()); } }
常用的對象存活性判斷方法有引用計數法與可達性分析,不過由于引用計數法無法解決對象循環引用的問題,因此主流的 JVM 傾向于使用可達性分析。
Reference Counting: 引用計數引用計數器在微軟的 COM 組件技術中、Adobe 的 ActionScript3 種都有使用。引用計數器的原理很簡單,對于一個對象 A,只要有任何一個對象引用了 A,則 A 的引用計數器就加 1,當引用失效時,引用計數器就減 1。只要對象 A 的引用計數器的值為 0,則對象 A 就不可能再被使用。引用計數器的實現也非常簡單,只需要為每個對象配置一個整形的計數器即可。但是引用計數器有一個嚴重的問題,即無法處理循環引用的情況。因此,在 Java 的垃圾回收器中沒有使用這種算法。一個簡單的循環引用問題描述如下:有對象 A 和對象 B,對象 A 中含有對象 B 的引用,對象 B 中含有對象 A 的引用。此時,對象 A 和對象 B 的引用計數器都不為 0。但是在系統中卻不存在任何第 3 個對象引用了 A 或 B。也就是說,A 和 B 是應該被回收的垃圾對象,但由于垃圾對象間相互引用,從而使垃圾回收器無法識別,引起內存泄漏。
所謂的引用樹本質上是有根的圖結構,它沿著對象的根句柄向下查找到活著的節點,并標記下來;其余沒有被標記的節點就是死掉的節點,這些對象就是可以被回收的,或者說活著的節點就是可以被拷貝走的,具體要看所在 HeapSize中 的區域以及算法,它的大致示意圖如下圖所示(注意這里是指針是單向的):
首先,所有回收器都會通過一個標記過程來對存活對象進行統計。JVM 中用到的所有現代 GC 算法在回收前都會先找出所有仍存活的對象。下圖中所展示的JVM中的內存布局可以用來很好地闡釋這一概念:
而所謂的GC根對象包括:當前執行方法中的所有本地變量及入參、活躍線程、已加載類中的靜態變量、JNI 引用。接下來,垃圾回收器會對內存中的整個對象圖進行遍歷,它先從 GC 根對象開始,然后是根對象引用的其它對象,比如實例變量。回收器將訪問到的所有對象都標記為存活。存活對象在上圖中被標記為藍色。當標記階段完成了之后,所有的存活對象都已經被標記完了。其它的那些(上圖中灰色的那些)也就是GC根對象不可達的對象,也就是說你的應用不會再用到它們了。這些就是垃圾對象,回收器將會在接下來的階段中清除它們。
不過那些發現不能到達 GC Roots 的對象并不會立即回收,在真正回收之前,對象至少要被標記兩次。當第一次被發現不可達時,該對象會被標記一次,同時調用此對象的 finalize()方法(如果有);在第二次被發現不可達后,對象被回收。利用 finalisze() 方法,對象可以逃離一次被回收的命運,但是只有一次。逃命方法如下,需要在 finalize() 方法中給自己加一個 GCRoots 中的 hook:
public class EscapeFromGC(){ public static EscapeFromGC hook; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize mehtod executed!"); EscapeFromGC.hook = this; }通用垃圾回收算法
算法名 | 優勢 | 缺陷 |
---|---|---|
Mark-Sweep / 標記-清除 | 簡單 | 效率低下且會產生很多不連續內存,分配大對象時,容易提前引起另一次垃圾回收。 |
Copying / 復制 | 效率較高,不用考慮內存碎片化 | 存在空間浪費 |
Mark-Compact / 標記-整理 | 避免了內存碎片化 | GC 暫停時間增長 |
標記-清除算法將垃圾回收分為兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段首先通過根節點,標記所有從根節點開始的較大對象。因此,未被標記的對象就是未被引用的垃圾對象。然后,在清除階段,清除所有未被標記的對象。該算法最大的問題是存在大量的空間碎片,因為回收后的空間是不連續的。在對象的堆空間分配過程中,尤其是大對象的內存分配,不連續的內存空間的工作效率要低于連續的空間。
從概念上來講,標記-清除算法使用的方法是最簡單的,只需要忽略這些對象便可以了。也就是說當標記階段完成之后,未被訪問到的對象所在的空間都會被認為是空閑的,可以用來創建新的對象。這種方法需要使用一個空閑列表來記錄所有的空閑區域以及大小。對空閑列表的管理會增加分配對象時的工作量。這種方法還有一個缺陷就是——雖然空閑區域的大小是足夠的,但卻可能沒有一個單一區域能夠滿足這次分配所需的大小,因此本次分配還是會失敗(在Java中就是一次 OutOfMemoryError)。
Copying: 復制算法將現有的內存空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。如果系統中的垃圾對象很多,復制算法需要復制的存活對象數量并不會太大。因此在真正需要垃圾回收的時刻,復制算法的效率是很高的。又由于對象在垃圾回收過程中統一被復制到新的內存空間中,因此,可確保回收后的內存空間是沒有碎片的。該算法的缺點是將系統內存折半。
Java 的新生代串行垃圾回收器中使用了復制算法的思想。新生代分為 eden 空間、from 空間、to 空間 3 個部分。其中 from 空間和 to 空間可以視為用于復制的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱為 survivor 空間,即幸存者空間,用于存放未被回收的對象。在垃圾回收時,eden 空間中的存活對象會被復制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕對象也會被復制到 to 空間中 (大對象,或者老年對象會直接進入老年帶,如果 to 空間已滿,則對象也會直接進入老年代)。此時,eden 空間和 from 空間中的剩余對象就是垃圾對象,可以直接清空,to 空間則存放此次回收后的存活對象。這種改進的復制算法既保證了空間的連續性,又避免了大量的內存空間浪費。
標記-復制算法與標記-整理算法非常類似,它們都會將所有存活對象重新進行分配。區別在于重新分配的目標地址不同,復制算法是為存活對象分配了另外的內存 區域作為它們的新家。標記復制算法的優點在于標記階段和復制階段可以同時進行。它的缺點是需要一塊能容納下所有存活對象的額外的內存空間。
Mark-Compact: 標記-壓縮算法復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在年輕代經常發生,但是在老年代更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由于存活的對象較多,復制的成本也將很高。
標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上做了一些優化。也首先需要從根節點開始對所有可達對象做一次標記,但之后,它并不簡單地 清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比比較高。
標記-壓縮算法修復了標記-清除算法的短板——它將所有標記的也就是存活的對象都移動到內存區域的開始位置。這種方法的缺點就是GC暫停的時間會增 長,因為你需要將所有的對象都拷貝到一個新的地方,還得更新它們的引用地址。相對于標記-清除算法,它的優點也是顯而易見的——經過整理之后,新對象的分 配只需要通過指針碰撞便能完成(pointer bumping),相當簡單。使用這種方法空閑區域的位置是始終可知的,也不會再有碎片的問題了。
Incremental Collecting: 增量回收算法在垃圾回收過程中,應用軟件將處于一種 CPU 消耗很高的狀態。在這種 CPU 消耗很高的狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。
增量算法現代垃圾回收的一個前身,其基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。依次反復,直到垃圾收集完成。使用這種方式,由于在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
Generational Collecting: 分代回收算法分代回收器是增量收集的另一個化身,根據垃圾回收對象的特性,不同階段最優的方式是使用合適的算法用于本階段的垃圾回收,分代算法即是基于這種思想,它將內存區間根據對象的特點分成幾塊,根據 每塊內存區間的特點,使用不同的回收算法,以提高垃圾回收的效率。以 Hot Spot 虛擬機為例,它將所有的新建對象都放入稱為年輕代的內存區域,年輕代的特點是對象會很快回收,因此,在年輕代就選擇效率較高的復制算法。當一個對象經過幾 次回收后依然存活,對象就會被放入稱為老生代的內存空間。在老生代中,幾乎所有的對象都是經過幾次垃圾回收后依然得以幸存的。因此,可以認為這些對象在一 段時期內,甚至在應用程序的整個生命周期中,將是常駐內存的。如果依然使用復制算法回收老生代,將需要復制大量對象。再加上老生代的回收性價比也要低于新 生代,因此這種做法也是不可取的。根據分代的思想,可以對老年代的回收使用與新生代不同的標記-壓縮算法,以提高垃圾回收效率。
Concurrent Collecting: 并發回收算法所謂的并發回收算法即是指垃圾回收器與應用程序能夠交替工作,并發回收 器其實也會暫停,但是時間非常短,它并不會在從開始回收尋找、標記、清楚、壓縮或拷貝等方式過程完全暫停服務,它發現有幾個時間比較長,一個就是標記,因 為這個回收一般面對的是老年代,這個區域一般很大,而一般來說絕大部分對象應該是活著的,所以標記時間很長,還有一個時間是壓縮,但是壓縮并不一定非要每 一次做完GC都去壓縮的,而拷貝呢一般不會用在老年代,所以暫時不考慮;所以他們想出來的辦法就是:第一次短暫停機是將所有對象的根指針找到,這個非常容 易找到,而且非常快速,找到后,此時GC開始從這些根節點標記活著的節點(這里可以采用并行),然后待標記完成后,此時可能有新的 內存申請以及被拋棄(java本身沒有內存釋放這一概念),此時JVM會記錄下這個過程中的增量信息,而對于老年代來說,必須要經過多次在 survivor倒騰后才會進入老年代,所以它在這段時間增量一般來說會非常少,而且它被釋放的概率前面也說并不大(JVM如果不是完全做Cache,自 己做pageCache而且發生概率不大不小的pageout和pagein是不適合的);JVM根據這些增量信息快速標記出內部的節點,也是非常快速 的,就可以開始回收了,由于需要殺掉的節點并不多,所以這個過程也非常快,壓縮在一定時間后會專門做一次操作,有關暫停時間在Hotspot版本,也就是 SUN的jdk中都是可以配置的,當在指定時間范圍內無法回收時,JVM將會對相應尺寸進行調整,如果你不想讓它調整,在設置各個區域的大小時,就使用定 量,而不要使用比例來控制;當采用并發回收算法的時候,一般對于老年代區域,不會等待內存小于10%左右的時候才會發起回收,因為并發回收是允許在回收的 時候被分配,那樣就有可能來不及了,所以并發回收的時候,JVM可能會在68%左右的時候就開始啟動對老年代GC了。
JVM 垃圾回收器對比1999 年隨 JDK1.3.1 一起來的是串行方式的 Serial GC,它是第一款垃圾回收器;此后,JDK1.4 和 J2SE1.3 相繼發布。2002 年 2 月 26 日,J2SE1.4 發布;Parallel GC 和Concurrent Mark Sweep (CMS)GC 跟隨 JDK1.4.2 一起發布,并且 Parallel GC 在 JDK6 之后成為 HotSpot 默認 GC。這三個垃圾回收器也是各有千秋,Serial GC 適合最小化地使用內存和并行開銷的場景、Parallel GC 適合最大化應用程序吞吐量的場景、CMS GC 適合最小化中斷或停頓時間的場景。上圖即展示了多種垃圾回收器之間的關系;不過隨著應用程序所應對的業務越來越龐大、復雜,用戶越來越多,沒有合適的回收器就不能保證應用程序正常進行,而經常造成 STW 停頓的回收器又跟不上實際的需求,所以才會不斷地嘗試對搜集器進行優化。Garbage First(G1)GC 正是面向這種業務需求所生,它是一個并行回收器,把堆內存分割為很多不相關的區間(Region);每個區間可以屬于老年代或者年輕代,并且每個年齡代區間可以是物理上不連續的。
名稱 | 作用域 | 算法 | 特性 | 設置 |
---|---|---|---|---|
Serial | Serial GC 作用于新生代,Serial Old GC 作用于老年代垃圾收集 | 二者皆采用了串行回收與 "Stop-the-World",Serial 使用的是復制算法,而 Serial Old 使用的是電俄式-標記壓縮算法 | 基于串行回收的垃圾回收器適用于大多數對于暫停時間要求不高的 Client 模式下的 JVM | 使用 -XX:+UserSerialGC 手動指定使用 Serial 回收器執行內存回收任務 |
Throughput/Parallel | Parallel 作用于新生代,Parallel Old 作用于老年代 | 并行回收和 "Stop-the-World",Parallel 使用的是復制算法,Parallel Old 使用的是標記-壓縮算法 | 程序吞吐量優先的應用場景中,在 Server 模式下內存回收的性能較為不錯 | 使用 -XX:+UseParallelGC 手動指定使用 Parallel 回收器執行內存回收任務 |
CMS,Concurrent-Mark-Sweep | 老年代垃圾回收器,又稱作 Mostly-Concurrent 回收器 | 使用了標記清除算法,分為初始標記( Initial-Mark,Stop-the-World )、并發標記( Concurrent-Mark )、再次標記( Remark,Stop-the-World )、并發清除( Concurrent-Sweep ) | 并發低延遲,吞吐量較低。經過CMS收集的堆會產生空間碎片,會帶來堆內存的浪費 | 使用 -XX:+UseConcMarkSweepGC 來手動指定使用 CMS 回收器執行內存回收任務 |
G1,Garbage First | 沒有采用傳統物理隔離的新生代和老年代的布局方式,僅僅以邏輯上劃分為新生代和老年代,選擇的將 Java 堆區劃分為 2048 個大小相同的獨立 Region 塊 | 使用了標記壓縮算法 | 基于并行和并發、低延遲以及暫停時間更加可控的區域化分代式服務器類型的垃圾回收器 | 使用 -XX:UseG1GC 來手動指定使用 G1 回收器執行內存回收任務 |
關于標記階段有幾個關鍵點是值得注意的:
開始進行標記前,需要先暫停應用線程,否則如果對象圖一直在變化的話是無法真正去遍歷它的。暫停應用線程以便 JVM 可以盡情地收拾家務的這種情況又被稱之為安全點(Safe Point),這會觸發一次Stop The World(STW)暫停。觸發安全點的原因有許多,但最常見的應該就是垃圾回收了。
暫停時間的長短并不取決于堆內對象的多少也不是堆的大小,而是存活對象的多少。因此,調高堆的大小并不會影響到標記階段的時間長短。
當標記階段完成后,GC開始進入下一階段,刪除不可達對象。
Serial GC串行回收器主要有兩個特點:第一,它僅僅使用單線程進行垃圾回收;第二,它獨占式的垃圾回收。在串行回收器進行垃圾回收時,Java 應用程序中的線程都需要暫停,等待垃圾回收的完成,這樣給用戶體驗造成較差效果。雖然如此,串行回收器卻是一個成熟、經過長時間生產環境考驗的極為高效的 回收器。新生代串行處理器使用復制算法,實現相對簡單,邏輯處理特別高效,且沒有線程切換的開銷。在諸如單 CPU 處理器或者較小的應用內存等硬件平臺不是特別優越的場合,它的性能表現可以超過并行回收器和并發回收器。在 HotSpot 虛擬機中,使用-XX:+UseSerialGC 參數可以指定使用新生代串行回收器和老年代串行回收器。當 JVM 在 Client 模式下運行時,它是默認的垃圾回收器。老年代串行回收器使用的是標記-壓縮算法。和新生代串行回收器一樣,它也是一個串行的、獨占式的垃圾回收器。由于老年代垃圾回收通常會使用比新生代垃圾回 收更長的時間,因此,在堆空間較大的應用程序中,一旦老年代串行回收器啟動,應用程序很可能會因此停頓幾秒甚至更長時間。雖然如此,老年代串行回收器可以 和多種新生代回收器配合使用,同時它也可以作為 CMS 回收器的備用回收器。若要啟用老年代串行回收器,可以嘗試使用以下參數:-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。
Serial GC 的工作步驟如下所示:
ParNew GC并行回收器是工作在新生代的垃圾回收器,它只簡單地將串行回收器多線程化。它的回收策略、算法以及參數和串行回收器一樣。
并行回收器 也是獨占式的回收器,在收集過程中,應用程序會全部暫停。但由于并行回收器使用多線程進行垃圾回收,因此,在并發能力比較強的 CPU 上,它產生的停頓時間要短于串行回收器,而在單 CPU 或者并發能力較弱的系統中,并行回收器的效果不會比串行回收器好,由于多線程的壓力,它的實際表現很可能比串行回收器差。開啟并行回收器可以使用參數-XX:+UseParNewGC,該參數設置新生代使用并行回收器,老年代使用串行回收器。老年代的并行回收回收器也是一種多線程并發的回收器。和新生代并行回收回收器一樣,它也是一種關注吞吐量的回收器。老年代并行回收回收器使用標記-壓縮算法,JDK1.6 之后開始啟用。
Parallel Scavenge 收集器的特點是它的關注點與其他收集器不同,CMS 等收集器的關注點盡可能地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput)。Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK 1.6中才開始提供的。使用 -XX:+UseParallelOldGC 可以在新生代和老生代都使用并行回收回收器,這是一對非常關注吞吐量的垃圾回收器組合,在對吞吐量敏感的系統中,可以考慮使用。參數 -XX:ParallelGCThreads 也可以用于設置垃圾回收時的線程數量。
Parallel GC 的工作步驟如下所示:
CMS( Concurrent Mark-Sweep ) 是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器,適用于對停頓比較敏感,并且有相對較多存活時間較長的對象(老年代較大)的應用程序;不過 CMS 雖然減少了回收的停頓時間,但是降低了堆空間的利用率。CMS GC 采用了 Mark-Sweep 算法,因此經過CMS收集的堆會產生空間碎片;為了解決堆空間浪費問題,CMS回收器不再采用簡單的指針指向一塊可用堆空間來為下次對象分配使用。而是把一些未分配的空間匯總成一個列表,當 JVM 分配對象空間的時候,會搜索這個列表找到足夠大的空間來存放住這個對象。另一方面,由于 CMS 線程和應用程序線程并發執行,CMS GC 需要更多的 CPU 資源。同時,因為CMS標記階段應用程序的線程還是在執行的,那么就會有堆空間繼續分配的情況,為了保證在CMS回收完堆之前還有空間分配給正在運行的應用程序,必須預留一部分空間。也就是說,CMS不會在老年代滿的時候才開始收集。相反,它會嘗試更早的開始收集,已避免上面提到的情況:在回收完成之前,堆沒有足夠空間分配!默認當老年代使用68%的時候,CMS就開始行動了。 – XX:CMSInitiatingOccupancyFraction =n 來設置這個閥值。
CMS GC 工作步驟如下所示:
初始標記(STW initial mark):在這個階段,需要虛擬機停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的"根對象"開始,只掃描到能夠和"根對象"直接關聯的對象,并作標記。所以這個過程雖然暫停了整個JVM,但是很快就完成了。
并發標記(Concurrent marking):這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。并發標記階段,應用程序的線程和并發標記的線程并發執行,所以用戶不會感受到停頓。
并發預清理(Concurrent precleaning):并發預清理階段仍然是并發的。在這個階段,虛擬機查找在執行并發標記階段新進入老年代的對象(可能會有一些對象從新生代晉升到老年代,或者有一些對象被分配到老年代)。通過重新掃描,減少下一個階段"重新標記"的工作,因為下一個階段會Stop The World。
重新標記(STW remark):這個階段會暫停虛擬機,回收器線程掃描在CMS堆中剩余的對象。掃描從"跟對象"開始向下追溯,并處理對象關聯。
并發清理(Concurrent sweeping):清理垃圾對象,這個階段回收器線程和應用程序線程并發執行。
并發重置(Concurrent reset):這個階段,重置CMS回收器的數據結構,等待下一次垃圾回收。
G1 GCG1 GC 是 JDK 1.7 中正式投入使用的用于取代 CMS 的壓縮回收器,它雖然沒有在物理上隔斷新生代與老生代,但是仍然屬于分代垃圾回收器;G1 GC 仍然會區分年輕代與老年代,年輕代依然分有 Eden 區與 Survivor 區。G1 GC 首先將堆分為大小相等的 Region,避免全區域的垃圾收集,然后追蹤每個 Region 垃圾堆積的價值大小,在后臺維護一個優先列表,根據允許的收集時間優先回收價值最大的Region;同時 G1 GC 采用 Remembered Set 來存放 Region 之間的對象引用以及其他回收器中的新生代與老年代之間的對象引用,從而避免全堆掃描。G1 GC 的分區示例如下圖所示:
隨著 G1 GC 的出現,Java 垃圾回收器通過引入 Region 的概念,從傳統的連續堆內存布局設計,逐步走向了物理上不連續但是邏輯上依舊連續的內存塊;這樣我們能夠將某個 Region 動態地分配給 Eden、Survivor、老年代、大對象空間、空閑區間等任意一個。每個 Region 都有一個關聯的 Remembered Set(簡稱RS),RS 的數據結構是 Hash 表,里面的數據是 Card Table (堆中每 512byte 映射在 card table 1byte)。簡單的說RS里面存在的是Region中存活對象的指針。當Region中數據發生變化時,首先反映到Card Table中的一個或多個Card上,RS通過掃描內部的Card Table得知Region中內存使用情況和存活對象。在使用Region過程中,如果Region被填滿了,分配內存的線程會重新選擇一個新的Region,空閑Region被組織到一個基于鏈表的數據結構(LinkedList)里面,這樣可以快速找到新的Region。
總結而言,G1 GC 的特性如下:
并行性:G1在回收期間,可以有多個GC線程同時工作,有效利用多核計算能力;
并發性:G1擁有與應用程序交替執行的能力,部分工作可以和應用程序同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程序的情況;
分代GC:G1依然是一個分代回收器,但是和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;
空間整理:G1在回收過程中,會進行適當的對象移動,不像CMS只是簡單地標記清理對象。在若干次GC后,CMS必須進行一次碎片整理。而G1不同,它每次回收都會有效地復制對象,減少空間碎片,進而提升內部循環速度。
可預見性:為了縮短停頓時間,G1建立可預存停頓的模型,這樣在用戶設置的停頓時間范圍內,G1會選擇適當的區域進行收集,確保停頓時間不超過用戶指定時間。
G1 GC 的工作步驟如下所示:
初始標記(標記一下GC Roots能直接關聯的對象并修改TAMS值,需要STW但耗時很短)
并發標記(從GC Root從堆中對象進行可達性分析找存活的對象,耗時較長但可以與用戶線程并發執行)
最終標記(為了修正并發標記期間產生變動的那一部分標記記錄,這一期間的變化記錄在Remembered Set Log 里,然后合并到Remembered Set里,該階段需要STW但是可并行執行)
篩選回收(對各個Region回收價值排序,根據用戶期望的GC停頓時間制定回收計劃來回收)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67537.html
摘要:在這種消耗很高的狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。 Java 垃圾回收(GC) 泛讀 文章地址: https://segmentfault.com/a/1190000008922319 0. 序言 帶著問題去看待 垃圾回收(GC) 會比較好,一般來說主要的...
垃圾回收(GC)是JVM的一大殺器,它使程序員可以更高效地專注于程序的開發設計,而不用過多地考慮對象的創建銷毀等操作。但是這并不是說程序員不需要了解GC。GC只是Java編程中一項自動化工具,任何一個工具都有它適用的范圍,當超出它的范圍的時候,可能它將不是那么自動,而是需要人工去了解與適應地適用。 擁有一定工作年限的程序員,在工作期間肯定會經常碰到像內存溢出、內存泄露、高并發的場景。這時候在應對這...
摘要:一次性編譯成機器碼,脫離開發環境獨立運行,運行效率較高。解釋型語言使用專門的解釋器對源程序逐行解釋成特定平臺的機器碼并立即執行的語言。垃圾回收機制保護程序的完整性,垃圾回收是語言安全性策略的一個重要部分。 Java程序運行機制 編譯型語言 使用專門的編譯器,針對特定平臺(操作系統)將某種高級語言源代碼一次性翻譯成可被該平臺硬件執行的機器碼(包括機器指令和操作數),并包裝成該平臺所能識...
摘要:這個算法看似不錯而且簡單,不過存在這一個致命傷當兩個對象互相引用的時候,就永遠不會被回收于是引用計數算法就永遠回收不了這兩個對象,下面介紹另一種算法。 前言 ? 如果要問Java與其他編程語言最大的不同是什么,我第一個想到的一定就是Java所運行的JVM所自帶的自動垃圾回收機制,以下是我學習JVM垃圾回收機制整理的筆記,希望能對讀者有一些幫助。 哪些內存需要回收?what? ? ...
閱讀 725·2021-11-17 09:33
閱讀 3757·2021-09-01 10:46
閱讀 1751·2019-08-30 11:02
閱讀 3280·2019-08-29 15:05
閱讀 1396·2019-08-26 11:39
閱讀 2272·2019-08-23 17:04
閱讀 1973·2019-08-23 15:43
閱讀 1371·2019-08-23 14:12