Java 語言的一大特色就是能夠進行自動垃圾回收處理,而無需開發人員過於關注系統資源,例如內存資源的釋放狀況。自動垃圾收集雖然大大減輕了開發人員的工做量,可是也增長了軟件系統的負擔。 java
擁有垃圾收集器能夠說是 Java 語言與 C++語言的一項顯著區別。在 C++語言中,程序員必須當心謹慎地處理每一項內存分配,且內存使用完後必須手工釋放曾經佔用的內存空間。當內存釋放不夠徹底時,即存在分配但永不釋放的內存塊,就會引發內存泄漏,嚴重時甚至致使程序癱瘓。 程序員
如下列舉了垃圾回收器經常使用的算法及實驗原理: 算法
引用計數法 (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 是應該被回收的垃圾對象,但因爲垃圾對象間相互引用,從而使垃圾回收器沒法識別,引發內存泄漏。 jvm
標記-清除算法 (Mark-Sweep) 性能
標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段首先經過根節點,標記全部從根節點開始的較大對象。所以,未被標記的對象就是未被引用的垃圾對象。而後,在清除階段,清除全部未被標記的對象。該算法最大的問題是存在大量的空間碎片,由於回收後的空間是不連續的。在對象的堆空間分配過程當中,尤爲是大對象的內存分配,不連續的內存空間的工做效率要低於連續的空間。 測試
複製算法 (Copying)
將現有的內存空間分爲兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。
若是系統中的垃圾對象不少,複製算法須要複製的存活對象數量並不會太大。所以在真正須要垃圾回收的時刻,複製算法的效率是很高的。又因爲對象在垃圾回收過程當中統一被複制到新的內存空間中,所以,可確保回收後的內存空間是沒有碎片的。該算法的缺點是將系統內存摺半。
Java 的新生代串行垃圾回收器中使用了複製算法的思想。新生代分爲 eden 空間、from 空間、to 空間 3 個部分。其中 from 空間和 to 空間能夠視爲用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱爲 survivor 空間,即倖存者空間,用於存放未被回收的對象。
在垃圾回收時,eden 空間中的存活對象會被複制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕對象也會被複制到 to 空間中 (大對象,或者老年對象會直接進入老年帶,若是 to 空間已滿,則對象也會直接進入老年代)。此時,eden 空間和 from 空間中的剩餘對象就是垃圾對象,能夠直接清空,to 空間則存放這次回收後的存活對象。這種改進的複製算法既保證了空間的連續性,又避免了大量的內存空間浪費。
標記-壓縮算法 (Mark-Compact)
複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在年輕代常常發生,可是在老年代更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活的對象較多,複製的成本也將很高。
標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上作了一些優化。也首先須要從根節點開始對全部可達對象作一次標記,但以後,它並不簡單地清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。以後,清理邊界外全部的空間。這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。
增量算法 (Incremental Collecting)
在垃圾回收過程當中,應用軟件將處於一種 CPU 消耗很高的狀態。在這種 CPU 消耗很高的狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。
增量算法的基本思想是,若是一次性將全部的垃圾進行處理,須要形成系統長時間的停頓,那麼就可讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。可是,由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。
分代 (Generational Collecting)
根據垃圾回收對象的特性,不一樣階段最優的方式是使用合適的算法用於本階段的垃圾回收,分代算法便是基於這種思想,它將內存區間根據對象的特色分紅幾塊,根據每塊內存區間的特色,使用不一樣的回收算法,以提升垃圾回收的效率。以 Hot Spot 虛擬機爲例,它將全部的新建對象都放入稱爲年輕代的內存區域,年輕代的特色是對象會很快回收,所以,在年輕代就選擇效率較高的複製算法。當一個對象通過幾回回收後依然存活,對象就會被放入稱爲老生代的內存空間。在老生代中,幾乎全部的對象都是通過幾回垃圾回收後依然得以倖存的。所以,能夠認爲這些對象在一段時期內,甚至在應用程序的整個生命週期中,將是常駐內存的。若是依然使用複製算法回收老生代,將須要複製大量對象。再加上老生代的回收性價比也要低於新生代,所以這種作法也是不可取的。根據分代的思想,能夠對老年代的回收使用與新生代不一樣的標記-壓縮算法,以提升垃圾回收效率。
從不一樣角度分析垃圾收集器,能夠將其分爲不一樣的類型。
1. 按線程數分,能夠分爲串行垃圾回收器和並行垃圾回收器。串行垃圾回收器一次只使用一個線程進行垃圾回收;並行垃圾回收器一次將開啓多個線程同時進行垃圾回收。在並行能力較強的 CPU 上,使用並行垃圾回收器能夠縮短 GC 的停頓時間。
2. 按照工做模式分,能夠分爲併發式垃圾回收器和獨佔式垃圾回收器。併發式垃圾回收器與應用程序線程交替工做,以儘量減小應用程序的停頓時間;獨佔式垃圾回收器 (Stop the world) 一旦運行,就中止應用程序中的其餘全部線程,直到垃圾回收過程徹底結束。
3. 按碎片處理方式可分爲壓縮式垃圾回收器和非壓縮式垃圾回收器。壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片;非壓縮式的垃圾回收器不進行這步操做。
4. 按工做的內存區間,又可分爲新生代垃圾回收器和老年代垃圾回收器。
能夠用如下指標評價一個垃圾處理器的好壞。
吞吐量:指在應用程序的生命週期內,應用程序所花費的時間和系統總運行時間的比值。系統總運行時間=應用程序耗時+GC 耗時。若是系統運行了 100min,GC 耗時 1min,那麼系統的吞吐量就是 (100-1)/100=99%。
垃圾回收器負載:和吞吐量相反,垃圾回收器負載指來記回收器耗時與系統運行總時間的比值。
停頓時間:指垃圾回收器正在運行時,應用程序的暫停時間。對於獨佔回收器而言,停頓時間可能會比較長。使用併發的回收器時,因爲垃圾回收器和應用程序交替運行,程序的停頓時間會變短,可是,因爲其效率極可能不如獨佔垃圾回收器,故系統的吞吐量可能會較低。
垃圾回收頻率:指垃圾回收器多長時間會運行一次。通常來講,對於固定的應用而言,垃圾回收器的頻率應該是越低越好。一般增大堆空間能夠有效下降垃圾回收發生的頻率,可是可能會增長回收產生的停頓時間。
反應時間:指當一個對象被稱爲垃圾後多長時間內,它所佔據的內存空間會被釋放。
堆分配:不一樣的垃圾回收器對堆內存的分配方式多是不一樣的。一個良好的垃圾收集器應該有一個合理的堆內存區間劃分。
新生代串行收集器
串行收集器主要有兩個特色:第一,它僅僅使用單線程進行垃圾回收;第二,它獨佔式的垃圾回收。
在串行收集器進行垃圾回收時,Java 應用程序中的線程都須要暫停,等待垃圾回收的完成,這樣給用戶體驗形成較差效果。雖然如此,串行收集器倒是一個成熟、通過長時間生產環境考驗的極爲高效的收集器。新生代串行處理器使用複製算法,實現相對簡單,邏輯處理特別高效,且沒有線程切換的開銷。在諸如單 CPU 處理器或者較小的應用內存等硬件平臺不是特別優越的場合,它的性能表現能夠超過並行回收器和併發回收器。在 HotSpot 虛擬機中,使用-XX:+UseSerialGC 參數能夠指定使用新生代串行收集器和老年代串行收集器。當 JVM 在 Client 模式下運行時,它是默認的垃圾收集器。一次新生代串行收集器的工做輸出日誌相似如清單 1 信息 (使用-XX:+PrintGCDetails 開關) 所示。
[GC [DefNew: 3468K->150K(9216K), 0.0028638 secs][Tenured: 1562K->1712K(10240K), 0.0084220 secs] 3468K->1712K(19456K), [Perm : 377K->377K(12288K)], 0.0113816 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
它顯示了一次垃圾回收前的新生代的內存佔用量和垃圾回收後的新生代內存佔用量,以及垃圾回收所消耗的時間。
老年代串行收集器
老年代串行收集器使用的是標記-壓縮算法。和新生代串行收集器同樣,它也是一個串行的、獨佔式的垃圾回收器。因爲老年代垃圾回收一般會使用比新生代垃圾回收更長的時間,所以,在堆空間較大的應用程序中,一旦老年代串行收集器啓動,應用程序極可能會所以停頓幾秒甚至更長時間。雖然如此,老年代串行回收器能夠和多種新生代回收器配合使用,同時它也能夠做爲 CMS 回收器的備用回收器。若要啓用老年代串行回收器,能夠嘗試使用如下參數:-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。
Heap def new generation total 4928K, used 1373K [0x27010000, 0x27560000, 0x2c560000) eden space 4416K, 31% used [0x27010000, 0x27167628, 0x27460000) from space 512K, 0% used [0x27460000, 0x27460000, 0x274e0000) to space 512K, 0% used [0x274e0000, 0x274e0000, 0x27560000) tenured generation total 10944K, used 0K [0x2c560000, 0x2d010000, 0x37010000) the space 10944K, 0% used [0x2c560000, 0x2c560000, 0x2c560200, 0x2d010000) compacting perm gen total 12288K, used 376K [0x37010000, 0x37c10000, 0x3b010000) the space 12288K, 3% used [0x37010000, 0x3706e0b8, 0x3706e200, 0x37c10000) ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000) rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
若是使用-XX:+UseParNewGC 參數設置,表示新生代使用並行收集器,老年代使用串行收集器,如清單 3 所示。
Heap par new generation total 4928K, used 1373K [0x0f010000, 0x0f560000, 0x14560000) eden space 4416K, 31% used [0x0f010000, 0x0f167620, 0x0f460000) from space 512K, 0% used [0x0f460000, 0x0f460000, 0x0f4e0000) to space 512K, 0% used [0x0f4e0000, 0x0f4e0000, 0x0f560000) tenured generation total 10944K, used 0K [0x14560000, 0x15010000, 0x1f010000) the space 10944K, 0% used [0x14560000, 0x14560000, 0x14560200, 0x15010000) compacting perm gen total 12288K, used 2056K [0x1f010000, 0x1fc10000, 0x23010000) the space 12288K, 16% used [0x1f010000, 0x1f2121d0, 0x1f212200, 0x1fc10000) No shared spaces configured.
若是使用-XX:+UseParallelGC 參數設置,表示新生代和老年代均使用並行回收收集器。如清單 4 所示。
[Full GC [Tenured: 1712K->1699K(10240K), 0.0071963 secs] 1712K->1699K(19456K), [Perm : 377K->372K(12288K)], 0.0072393 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
它顯示了垃圾回收前老年代和永久區的內存佔用量,以及垃圾回收後老年代和永久區的內存使用量。
並行收集器
並行收集器是工做在新生代的垃圾收集器,它只簡單地將串行回收器多線程化。它的回收策略、算法以及參數和串行回收器同樣。
並行回收器也是獨佔式的回收器,在收集過程當中,應用程序會所有暫停。但因爲並行回收器使用多線程進行垃圾回收,所以,在併發能力比較強的 CPU 上,它產生的停頓時間要短於串行回收器,而在單 CPU 或者併發能力較弱的系統中,並行回收器的效果不會比串行回收器好,因爲多線程的壓力,它的實際表現極可能比串行回收器差。
開啓並行回收器可使用參數-XX:+UseParNewGC,該參數設置新生代使用並行收集器,老年代使用串行收集器。
[GC [ParNew: 825K->161K(4928K), 0.0155258 secs][Tenured: 8704K->661K(10944K), 0.0071964 secs] 9017K->661K(15872K), [Perm : 2049K->2049K(12288K)], 0.0228090 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] Heap par new generation total 4992K, used 179K [0x0f010000, 0x0f570000, 0x14560000) eden space 4480K, 4% used [0x0f010000, 0x0f03cda8, 0x0f470000) from space 512K, 0% used [0x0f470000, 0x0f470000, 0x0f4f0000) to space 512K, 0% used [0x0f4f0000, 0x0f4f0000, 0x0f570000) tenured generation total 10944K, used 8853K [0x14560000, 0x15010000, 0x1f010000) the space 10944K, 80% used [0x14560000, 0x14e057c0, 0x14e05800, 0x15010000) compacting perm gen total 12288K, used 2060K [0x1f010000, 0x1fc10000, 0x23010000) the space 12288K, 16% used [0x1f010000, 0x1f213228, 0x1f213400, 0x1fc10000) No shared spaces configured.
設置參數-XX:+UseConcMarkSweepGC 能夠要求新生代使用並行收集器,老年代使用 CMS。
[GC [ParNew: 8967K->669K(14784K), 0.0040895 secs] 8967K->669K(63936K), 0.0043255 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap par new generation total 14784K, used 9389K [0x03f50000, 0x04f50000, 0x04f50000) eden space 13184K, 66% used [0x03f50000, 0x047d3e58, 0x04c30000) from space 1600K, 41% used [0x04dc0000, 0x04e67738, 0x04f50000) to space 1600K, 0% used [0x04c30000, 0x04c30000, 0x04dc0000) concurrent mark-sweep generation total 49152K, used 0K [0x04f50000, 0x07f50000, 0x09f50000) concurrent-mark-sweep perm gen total 12288K, used 2060K [0x09f50000, 0x0ab50000, 0x0df50000)
並行收集器工做時的線程數量可使用-XX:ParallelGCThreads 參數指定。通常,最好與 CPU 數量至關,避免過多的線程數影響垃圾收集性能。在默認狀況下,當 CPU 數量小於 8 個,ParallelGCThreads 的值等於 CPU 數量,大於 8 個,ParallelGCThreads 的值等於 3+[5*CPU_Count]/8]。如下測試顯示了筆者筆記本上運行 8 個線程時耗時最短,本人筆記本是 8 核 IntelCPU。
[GC [ParNew: 8967K->676K(14784K), 0.0036983 secs] 8967K->676K(63936K), 0.0037662 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap par new generation total 14784K, used 9395K [0x040e0000, 0x050e0000, 0x050e0000) eden space 13184K, 66% used [0x040e0000, 0x04963e58, 0x04dc0000) from space 1600K, 42% used [0x04f50000, 0x04ff9100, 0x050e0000) to space 1600K, 0% used [0x04dc0000, 0x04dc0000, 0x04f50000) concurrent mark-sweep generation total 49152K, used 0K [0x050e0000, 0x080e0000, 0x0a0e0000) concurrent-mark-sweep perm gen total 12288K, used 2060K [0x0a0e0000, 0x0ace0000, 0x0e0e0000)
[GC [ParNew: 8967K->664K(14784K), 0.0207327 secs] 8967K->664K(63936K), 0.0208077 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
[GC [ParNew: 8967K->667K(14784K), 0.2323704 secs] 8967K->667K(63936K), 0.2324778 secs] [Times: user=0.34 sys=0.02, real=0.23 secs]
Error occurred during initialization of VM Too small new size specified
新生代並行回收 (Parallel Scavenge) 收集器
新生代並行回收收集器也是使用複製算法的收集器。從表面上看,它和並行收集器同樣都是多線程、獨佔式的收集器。可是,並行回收收集器有一個重要的特色:它很是關注系統的吞吐量。
新生代並行回收收集器可使用如下參數啓用:
-XX:+UseParallelGC:新生代使用並行回收收集器,老年代使用串行收集器。
-XX:+UseParallelOldGC:新生代和老年代都是用並行回收收集器。
Heap PSYoungGen total 4800K, used 893K [0x1dac0000, 0x1e010000, 0x23010000) eden space 4160K, 21% used [0x1dac0000,0x1db9f570,0x1ded0000) from space 640K, 0% used [0x1df70000,0x1df70000,0x1e010000) to space 640K, 0% used [0x1ded0000,0x1ded0000,0x1df70000) ParOldGen total 19200K, used 16384K [0x13010000, 0x142d0000, 0x1dac0000) object space 19200K, 85% used [0x13010000,0x14010020,0x142d0000) PSPermGen total 12288K, used 2054K [0x0f010000, 0x0fc10000, 0x13010000) object space 12288K, 16% used [0x0f010000,0x0f2119c0,0x0fc10000)
新生代並行回收收集器可使用如下參數啓用:
-XX:+MaxGCPauseMills:設置最大垃圾收集停頓時間,它的值是一個大於 0 的整數。收集器在工做時會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。若是但願減小停頓時間,而把這個值設置得很小,爲了達到預期的停頓時間,JVM 可能會使用一個較小的堆 (一個小堆比一個大堆回收快),而這將致使垃圾回收變得很頻繁,從而增長了垃圾回收總時間,下降了吞吐量。
-XX:+GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。好比 GCTimeRatio 等於 19,則系統用於垃圾收集的時間不超過 1/(1+19)=5%。默認狀況下,它的取值是 99,即不超過 1%的時間用於垃圾收集。
除此以外,並行回收收集器與並行收集器另外一個不一樣之處在於,它支持一種自適應的 GC 調節策略,使用-XX:+UseAdaptiveSizePolicy 能夠打開自適應 GC 策略。在這種模式下,新生代的大小、eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。在手工調優比較困難的場合,能夠直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量 (GCTimeRatio) 和停頓時間 (MaxGCPauseMills),讓虛擬機本身完成調優工做。
Heap PSYoungGen total 4800K, used 893K [0x1dac0000, 0x1e010000, 0x23010000) eden space 4160K, 21% used [0x1dac0000,0x1db9f570,0x1ded0000) from space 640K, 0% used [0x1df70000,0x1df70000,0x1e010000) to space 640K, 0% used [0x1ded0000,0x1ded0000,0x1df70000) PSOldGen total 19200K, used 16384K [0x13010000, 0x142d0000, 0x1dac0000) object space 19200K, 85% used [0x13010000,0x14010020,0x142d0000) PSPermGen total 12288K, used 2054K [0x0f010000, 0x0fc10000, 0x13010000) object space 12288K, 16% used [0x0f010000,0x0f2119c0,0x0fc10000)
它也顯示了收集器的工做成果,也就是回收前的內存大小和回收後的內存大小,以及花費的時間。
老年代並行回收收集器
老年代的並行回收收集器也是一種多線程併發的收集器。和新生代並行回收收集器同樣,它也是一種關注吞吐量的收集器。老年代並行回收收集器使用標記-壓縮算法,JDK1.6 以後開始啓用。
使用-XX:+UseParallelOldGC 能夠在新生代和老生代都使用並行回收收集器,這是一對很是關注吞吐量的垃圾收集器組合,在對吞吐量敏感的系統中,能夠考慮使用。參數-XX:ParallelGCThreads 也能夠用於設置垃圾回收時的線程數量。
清單 13 是設置線程數量爲 100 時老年代並行回收收集器輸出日誌:
Heap PSYoungGen total 4800K, used 893K [0x1dac0000, 0x1e010000, 0x23010000) eden space 4160K, 21% used [0x1dac0000,0x1db9f570,0x1ded0000) from space 640K, 0% used [0x1df70000,0x1df70000,0x1e010000) to space 640K, 0% used [0x1ded0000,0x1ded0000,0x1df70000) ParOldGen total 19200K, used 16384K [0x13010000, 0x142d0000, 0x1dac0000) object space 19200K, 85% used [0x13010000,0x14010020,0x142d0000) PSPermGen total 12288K, used 2054K [0x0f010000, 0x0fc10000, 0x13010000) object space 12288K, 16% used [0x0f010000,0x0f2119c0,0x0fc10000)
CMS 收集器
與並行回收收集器不一樣,CMS 收集器主要關注於系統停頓時間。CMS 是 Concurrent Mark Sweep 的縮寫,意爲併發標記清除,從名稱上能夠得知,它使用的是標記-清除算法,同時它又是一個使用多線程併發回收的垃圾收集器。
CMS 工做時,主要步驟有:初始標記、併發標記、從新標記、併發清除和併發重置。其中初始標記和從新標記是獨佔系統資源的,而併發標記、併發清除和併發重置是能夠和用戶線程一塊兒執行的。所以,從總體上來講,CMS 收集不是獨佔式的,它能夠在應用程序運行過程當中進行垃圾回收。
根據標記-清除算法,初始標記、併發標記和從新標記都是爲了標記出須要回收的對象。併發清理則是在標記完成後,正式回收垃圾對象;併發重置是指在垃圾回收完成後,從新初始化 CMS 數據結構和數據,爲下一次垃圾回收作好準備。併發標記、併發清理和併發重置都是能夠和應用程序線程一塊兒執行的。
CMS 收集器在其主要的工做階段雖然沒有暴力地完全暫停應用程序線程,可是因爲它和應用程序線程併發執行,相互搶佔 CPU,因此在 CMS 執行期內對應用程序吞吐量形成必定影響。CMS 默認啓動的線程數是 (ParallelGCThreads+3)/4),ParallelGCThreads 是新生代並行收集器的線程數,也能夠經過-XX:ParallelCMSThreads 參數手工設定 CMS 的線程數量。當 CPU 資源比較緊張時,受到 CMS 收集器線程的影響,應用程序的性能在垃圾回收階段可能會很是糟糕。
因爲 CMS 收集器不是獨佔式的回收器,在 CMS 回收過程當中,應用程序仍然在不停地工做。在應用程序工做過程當中,又會不斷地產生垃圾。這些新生成的垃圾在當前 CMS 回收過程當中是沒法清除的。同時,由於應用程序沒有中斷,因此在 CMS 回收過程當中,還應該確保應用程序有足夠的內存可用。所以,CMS 收集器不會等待堆內存飽和時才進行垃圾回收,而是當前堆內存使用率達到某一閾值時,便開始進行回收,以確保應用程序在 CMS 工做過程當中依然有足夠的空間支持應用程序運行。
這個回收閾值可使用-XX:CMSInitiatingOccupancyFraction 來指定,默認是 68。即當老年代的空間使用率達到 68%時,會執行一次 CMS 回收。若是應用程序的內存使用率增加很快,在 CMS 的執行過程當中,已經出現了內存不足的狀況,此時,CMS 回收將會失敗,JVM 將啓動老年代串行收集器進行垃圾回收。若是這樣,應用程序將徹底中斷,直到垃圾收集完成,這時,應用程序的停頓時間可能很長。所以,根據應用程序的特色,能夠對-XX:CMSInitiatingOccupancyFraction 進行調優。若是內存增加緩慢,則能夠設置一個稍大的值,大的閾值能夠有效下降 CMS 的觸發頻率,減小老年代回收的次數能夠較爲明顯地改善應用程序性能。反之,若是應用程序內存使用率增加很快,則應該下降這個閾值,以免頻繁觸發老年代串行收集器。
標記-清除算法將會形成大量內存碎片,離散的可用空間沒法分配較大的對象。在這種狀況下,即便堆內存仍然有較大的剩餘空間,也可能會被迫進行一次垃圾回收,以換取一塊可用的連續內存,這種現象對系統性能是至關不利的,爲了解決這個問題,CMS 收集器還提供了幾個用於內存壓縮整理的算法。
-XX:+UseCMSCompactAtFullCollection 參數可使 CMS 在垃圾收集完成後,進行一次內存碎片整理。內存碎片的整理並非併發進行的。-XX:CMSFullGCsBeforeCompaction 參數能夠用於設定進行多少次 CMS 回收後,進行一次內存壓縮。
-XX:CMSInitiatingOccupancyFraction 設置爲 100,同時設置-XX:+UseCMSCompactAtFullCollection 和-XX:CMSFullGCsBeforeCompaction,日誌輸出以下:
[GC [DefNew: 825K->149K(4928K), 0.0023384 secs][Tenured: 8704K->661K(10944K), 0.0587725 secs] 9017K->661K(15872K), [Perm : 374K->374K(12288K)], 0.0612037 secs] [Times: user=0.01 sys=0.02, real=0.06 secs] Heap def new generation total 4992K, used 179K [0x27010000, 0x27570000, 0x2c560000) eden space 4480K, 4% used [0x27010000, 0x2703cda8, 0x27470000) from space 512K, 0% used [0x27470000, 0x27470000, 0x274f0000) to space 512K, 0% used [0x274f0000, 0x274f0000, 0x27570000) tenured generation total 10944K, used 8853K [0x2c560000, 0x2d010000, 0x37010000) the space 10944K, 80% used [0x2c560000, 0x2ce057c8, 0x2ce05800, 0x2d010000) compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000) the space 12288K, 3% used [0x37010000, 0x3706db10, 0x3706dc00, 0x37c10000) ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000) rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
G1 收集器 (Garbage First)
G1 收集器的目標是做爲一款服務器的垃圾收集器,所以,它在吞吐量和停頓控制上,預期要優於 CMS 收集器。
與 CMS 收集器相比,G1 收集器是基於標記-壓縮算法的。所以,它不會產生空間碎片,也沒有必要在收集完成後,進行一次獨佔式的碎片整理工做。G1 收集器還能夠進行很是精確的停頓控制。它可讓開發人員指定當停頓時長爲 M 時,垃圾回收時間不超過 N。使用參數-XX:+UnlockExperimentalVMOptions –XX:+UseG1GC 來啓用 G1 回收器,設置 G1 回收器的目標停頓時間:-XX:MaxGCPauseMills=20,-XX:GCPauseIntervalMills=200。
經過清單 15 所示代碼運行 1 萬次循環,每次分配 512*100B 空間,採用不一樣的垃圾回收器,輸出程序運行所消耗的時間。
import java.util.HashMap; public class GCTimeTest { static HashMap map = new HashMap(); public static void main(String[] args){ long begintime = System.currentTimeMillis(); for(int i=0;i<10000;i++){ if(map.size()*512/1024/1024>=400){ map.clear();//保護內存不溢出 System.out.println("clean map"); } byte[] b1; for(int j=0;j<100;j++){ b1 = new byte[512]; map.put(System.nanoTime(), b1);//不斷消耗內存 } } long endtime = System.currentTimeMillis(); System.out.println(endtime-begintime); } }
使用參數-Xmx512M -Xms512M -XX:+UseParNewGC 運行代碼,輸出以下:
clean map 8565
cost time=1655
使用參數-Xmx512M -Xms512M -XX:+UseParallelOldGC –XX:ParallelGCThreads=8 運行代碼,輸出以下:
clean map 8798
cost time=1998
使用參數-Xmx512M -Xms512M -XX:+UseSerialGC 運行代碼,輸出以下:
clean map 8864
cost time=1717
使用參數-Xmx512M -Xms512M -XX:+UseConcMarkSweepGC 運行代碼,輸出以下:
clean map 8862
cost time=1530
上面例子的 GC 輸出能夠看出,採用不一樣的垃圾回收機制及設定不一樣的線程數,對於代碼段的總體執行時間有較大的影響。須要讀者有針對性地選用適合本身代碼段的垃圾回收機制。
1. 與串行回收器相關的參數
-XX:+UseSerialGC:在新生代和老年代使用串行回收器。
-XX:+SuivivorRatio:設置 eden 區大小和 survivor 區大小的比例。
-XX:+PretenureSizeThreshold:設置大對象直接進入老年代的閾值。當對象的大小超過這個值時,將直接在老年代分配。
-XX:MaxTenuringThreshold:設置對象進入老年代的年齡的最大值。每一次 Minor GC 後,對象年齡就加 1。任何大於這個年齡的對象,必定會進入老年代。
2. 與並行 GC 相關的參數
-XX:+UseParNewGC: 在新生代使用並行收集器。
-XX:+UseParallelOldGC: 老年代使用並行回收收集器。
-XX:ParallelGCThreads:設置用於垃圾回收的線程數。一般狀況下能夠和 CPU 數量相等。但在 CPU 數量比較多的狀況下,設置相對較小的數值也是合理的。
-XX:MaxGCPauseMills:設置最大垃圾收集停頓時間。它的值是一個大於 0 的整數。收集器在工做時,會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。
-XX:GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。
-XX:+UseAdaptiveSizePolicy:打開自適應 GC 策略。在這種模式下,新生代的大小,eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。
3. 與 CMS 回收器相關的參數
-XX:+UseConcMarkSweepGC: 新生代使用並行收集器,老年代使用 CMS+串行收集器。
-XX:+ParallelCMSThreads: 設定 CMS 的線程數量。
-XX:+CMSInitiatingOccupancyFraction:設置 CMS 收集器在老年代空間被使用多少後觸發,默認爲 68%。
-XX:+UseFullGCsBeforeCompaction:設定進行多少次 CMS 垃圾回收後,進行一次內存壓縮。
-XX:+CMSClassUnloadingEnabled:容許對類元數據進行回收。
-XX:+CMSParallelRemarkEndable:啓用並行重標記。
-XX:CMSInitatingPermOccupancyFraction:當永久區佔用率達到這一百分比後,啓動 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
-XX:UseCMSInitatingOccupancyOnly:表示只在到達閾值的時候,才進行 CMS 回收。
-XX:+CMSIncrementalMode:使用增量模式,比較適合單 CPU。
4. 與 G1 回收器相關的參數
-XX:+UseG1GC:使用 G1 回收器。
-XX:+UnlockExperimentalVMOptions:容許使用實驗性參數。
-XX:+MaxGCPauseMills:設置最大垃圾收集停頓時間。
-XX:+GCPauseIntervalMills:設置停頓間隔時間。
5. 其餘參數
-XX:+DisableExplicitGC: 禁用顯示 GC。
CMS(Concurrent Mark-Sweep)是以犧牲吞吐量爲代價來得到最短回收停頓時間的垃圾回收器。對於要求服務器響應速度的應用上,這種垃圾回收器很是適合。在啓動JVM參數加上-XX:+UseConcMarkSweepGC ,這個參數表示對於老年代的回收採用CMS。CMS採用的基礎算法是:標記—清除。
初始標記 :在這個階段,須要虛擬機停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的"根對象"開始,只掃描到可以和"根對象"直接關聯的對象,並做標記。因此這個過程雖然暫停了整個JVM,可是很快就完成了。
併發標記 :這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程序的線程和併發標記的線程併發執行,因此用戶不會感覺到停頓。
併發預清理 :併發預清理階段仍然是併發的。在這個階段,虛擬機查找在執行併發標記階段新進入老年代的對象(可能會有一些對象重新生代晉升到老年代, 或者有一些對象被分配到老年代)。經過從新掃描,減小下一個階段"從新標記"的工做,由於下一個階段會Stop The World。
從新標記 :這個階段會暫停虛擬機,收集器線程掃描在CMS堆中剩餘的對象。掃描從"跟對象"開始向下追溯,並處理對象關聯。
併發清理 :清理垃圾對象,這個階段收集器線程和應用程序線程併發執行。
併發重置 :這個階段,重置CMS收集器的數據結構,等待下一次垃圾回收。
CSM執行過程:
總得來講,CMS回收器減小了回收的停頓時間,可是下降了堆空間的利用率。
若是你的應用程序對停頓比較敏感,而且在應用程序運行的時候能夠提供更大的內存和更多的CPU(也就是硬件牛逼),那麼使用CMS來收集會給你帶來好處。還有,若是在JVM中,有相對較多存活時間較長的對象(老年代比較大)會更適合使用CMS。
JVM 的垃圾回收器有不少種,先看下面的圖例:
從名字能夠看出來這是一個單線程的垃圾收集器,這個單線程不只僅是使用一個 CPU 或一條收集線程去完成,並且在垃圾收集的時候必須暫停全部其餘的工做線程。
主要用在 Client 模式下的默認新生代收集器,優點是簡單而高效。
ParNew 其實就是 Serial 的多線程版本,除了使用多線程進行垃圾蒐集外,其他的控制參數、收集算法、對象分配規則、回收策略都與 Serial 同樣。
ParNew 是許多運行在 Server 模式下的虛擬機首選的新生代收集器。由於只有 Serial 和 ParNew 能夠與 CMS 組合使用。
ParNew 也是使用 -XX:+UseConcMarkSweepGC 選項後的默認新生代收集器,也可使用 -XX:+UseParNewGC 來指定。
Tips
Parallel Scavenge 是一個新生代收集器,也是使用複製算法,也是並行多線程。
Parallel Scavenge 與 ParNew 的不一樣點是, Parallel Scavenge 收集器的目標是達到一個可控制的吞吐量,而其餘的收集器關注的是儘量縮短垃圾收集時候用戶線程的停頓時間。吞吐量是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,虛擬機運行了 100 分鐘,垃圾回收消耗了 1 分鐘,那麼吞吐率就是 99%。
停頓時間短適合須要跟用戶交互的程序,高吞吐量則能夠高效利用 CPU 時間,主要適合後臺運算不須要太多交互的任務。
Parallel Scavenge 收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集啊停頓時間的 -XX:MaxGCPauseMilis 參數以及直接設置吞吐量大小的 -XX:GCTimeRatio 參數。
MaxGCPauseMilis 參數容許的值是一個大於 0 的毫秒數,收集器儘可能控制回收垃圾時間不超過設定值。可是這個值不要設置過小,由於 GC 停頓時間縮短是犧牲吞吐量和新生代空間來換取的。系統把新生代調小,收集 300M 確定比 500M 快,也直接致使了垃圾收集更加頻繁,原來10秒一次、每次停頓100ms,如今5秒收集一次,每次停頓70ms。停頓時間降低了,可是吞吐量也降低了。
GCTimeRatio 參數是 0-100 的整數,也就是垃圾收集時間佔中時間的比率的底數。若是參數設置成 19,那容許的最大 GC 時間就佔 1/(1+19) 即 5%。默認是 99 ,容許 1 / (1+99) 的 GC 時間。
-XX:+UseAdaptiveSizePolicy 參數打開後,不須要手動指定 Eden 與 Survivor 的比例、晉升老年代對象年齡等細節,虛擬機會自動調節大小。
Tips
不能與 CMS 組合使用。
Serial 收集器的老年代版,同樣是單線程,使用標記整理算法。
Parallel Old 是 Parallel Scavenge 的老年代版本,使用多線程了標記整理算法。
在 JDK 1.6 前,Parallel Scavenge 收集器的位置很尷尬,由於他不能與 CMS 組合,因此當新生代選擇了 Parallel Scavenge ,老年代必須選擇 Serial Old 收集器。