一、垃圾回收機制
1.1、垃圾回收機制的概述
Java語言中一個顯著的特點就是引入了垃圾回收機制,使c++程序員最頭疼的內存管理的問題迎刃而解,它使得Java程序員在編寫程序的時候不再需要考慮內存管理。由於有個垃圾回收機制,Java中的對象不再有“作用域”的概念,只有對象的引用才有“作用域”。垃圾回收可以有效的防止內存泄露,有效的使用空閑的內存。
ps:內存泄露是指該內存空間使用完畢之后未回收,在不涉及復雜數據結構的一般情況下,Java 的內存泄露表現為一個內存對象的生命周期超出了程序需要它的時間長度,我們有時也將其稱為“對象游離”。
1.2、垃圾回收簡要過程
這里必須點出一個很重要的誤區:不可達的對象並不會馬上就會被直接回收,而是至少要經過兩次標記的過程。
第一次被標記過的對象,會檢查該對象是否重寫了finalize()方法。如果重寫了該方法,則將其放入一個F-Query隊列中,否則,直接將對象加入“即將回收”集合。在第二次標記之前,F-Query隊列中的所有對象會逐個執行finalize()方法,但是不保證該隊列中所有對象的finalize()方法都能被執行,這是因為JVM創建一個低優先級的線程去運行此隊列中的方法,很可能在沒有遍歷完之前,就已經被剝奪了運行的權利。那么運行finalize()方法的意義何在呢?這是對象避免自己被清理的最后手段:如果在執行finalize()方法的過程中,使得此對象重新與GC Roots引用鏈相連,則會在第二次標記過程中將此對象從F-Query隊列中清除,避免在這次回收中被清除,恢復成了一個“正常”的對象。但顯然這種好事不能無限的發生,對於曾經執行過一次finalize()的對象來說,之后如果再被標記,則不會再執行finalize()方法,只能等待被清除的命運。之后,GC將對F-Queue中的對象進行第二次小規模的標記,將隊列中重新與GC Roots引用鏈恢復連接的對象清除出“即將回收”集合。所有此集合中的內容將被回收。
public class JVMDemo05 { public static void main(String[] args) { JVMDemo05 jvmDemo05 = new JVMDemo05(); //jvmDemo05 = null; System.gc(); } protected void finalize() throws Throwable { System.out.println("gc在回收對象..."); } }
1.3、finalize作用
Java技術使用finalize()方法在垃圾收集器將對象從內存中清除出去前,做必要的清理工作。這個方法是由垃圾收集器在確定這個對象沒有被引用時對這個對象調用的。它是在Object類中定義的,因此所有的類都繼承了它。子類覆蓋finalize()方法以整理系統資源或者執行其他清理工作。finalize()方法是在垃圾收集器刪除對象之前對這個對象調用的。
二、垃圾回收機制算法
2.1、引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不再被使用的,垃圾收集器將回收該對象使用的內存。
優點:
引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。
缺點:
無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0.而且每次加減非常浪費內存。
2.2、標記清除算法
標記-清除(Mark-Sweep)算法顧名思義,主要就是兩個動作,一個是標記,另一個就是清除。
標記就是根據特定的算法(如:引用計數算法,可達性分析算法等)標出內存中哪些對象可以回收,哪些對象還要繼續用。
標記指示回收,那就直接收掉;標記指示對象還能用,那就原地不動留下。
缺點:
標記與清除效率低;
清除之后內存會產生大量碎片;
2.3、復制算法
S0和s1將可用內存按容量分成大小相等的兩塊,每次只使用其中一塊,當這塊內存使用完了,就將還存活的對象復制到另一塊內存上去,然后把使用過的內存空間一次清理掉。這樣使得每次都是對其中一塊內存進行回收,內存分配時不用考慮內存碎片等復雜情況,只需要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
缺點:
可使用的內存降為原來一半。
復制算法用於在新生代垃圾回收
2.4、標記-壓縮算法
標記壓縮法在標記清除基礎之上做了優化,把存活的對象壓縮到內存一端,而后進行垃圾清理。(java中老年代使用的就是標記壓縮法)
2.5、分代收集算法
根據內存中對象的存活周期不同,將內存划分為幾塊,java的虛擬機中一般把內存划分為新生代和年老代,當新創建對象時一般在新生代中分配內存空間,當新生代垃圾收集器回收幾次之后仍然存活的對象會被移動到年老代內存中,當大對象在新生代中無法找到足夠的連續內存時也直接在年老代中創建。
對於新生代和老年代來說,新生代回收頻率很高,但是每次回收耗時很短,而老年代回收頻率較低,但是耗時會相對較長,所以應該盡量減少老年代的GC。
2.6、為什么老年代使用標記壓縮、新生代使用復制算法
垃圾回收時的停頓現象
垃圾回收的任務是識別和回收垃圾對象進行內存清理,為了讓垃圾回收器可以更高效的執行,大部分情況下,會要求系統進如一個停頓的狀態。停頓的目的是為了終止所有的應用線程,只有這樣的系統才不會有新垃圾的產生。同時停頓保證了系統狀態在某一個瞬間的一致性,也有利於更好的標記垃圾對象。因此在垃圾回收時,都會產生應用程序的停頓。
三、垃圾收集器
3.1、什么是Java垃圾回收器
Java垃圾回收器是Java虛擬機(JVM)的三個重要模塊(另外兩個是解釋器和多線程機制)之一,為應用程序提供內存的自動分配(Memory Allocation)、自動回收(Garbage Collect)功能,這兩個操作都發生在Java堆上(一段內存塊)。某一個時點,一個對象如果有一個以上的引用(Rreference)指向它,那么該對象就為活着的(Live),否則死亡(Dead),視為垃圾,可被垃圾回收器回收再利用。垃圾回收操作需要消耗CPU、線程、時間等資源,所以容易理解的是垃圾回收操作不是實時的發生(對象死亡馬上釋放),當內存消耗完或者是達到某一個指標(Threshold,使用內存占總內存的比列,比如0.75)時,觸發垃圾回收操作。有一個對象死亡的例外,java.lang.Thread類型的對象即使沒有引用,只要線程還在運行,就不會被回收。
3.2、串行回收器(Serial Collector)
單線程執行回收操作,回收期間暫停所有應用線程的執行,client模式下的默認回收器,通過-XX:+UseSerialGC命令行可選項強制指定。參數可以設置使用新生代串行和老年代串行回收器
年輕代的回收算法(Minor Collection):把Eden區的存活對象移到To區,To區裝不下直接移到年老代,把From區的移到To區,To區裝不下直接移到年老代,From區里面年齡很大的升級到年老代。回收結束之后,Eden和From區都為空,此時把From和To的功能互換,From變To,To變From,每一輪回收之前To都是空的。設計的選型為復制。
年老代的回收算法(Full Collection):年老代的回收分為三個步驟,標記(Mark)、清除(Sweep)、合並(Compact)。標記階段把所有存活的對象標記出來,清除階段釋放所有死亡的對象,合並階段把所有活着的對象合並到年老代的前部分,把空閑的片段都留到后面。設計的選型為合並,減少內存的碎片。
3.3、並行回收器(ParNew回收器)
並行回收器在串行回收器基礎上做了改進,他可以使用多個線程同時進行垃圾回收,對於計算能力強的計算機而言,可以有效的縮短垃圾回收所需的尖際時間。
ParNew回收器是一個工作在新生代的垃圾收集器,他只是簡單的將串行回收器多線程快他的回收策略和算法和串行回收器一樣。使用XX:+UseParNewGC 新生代ParNew回收器,老年代則使用串行回收器
ParNew回收器工作時的線程數量可以使用XX:ParaleiGCThreads參數指定,一般最好和計算機的CPU相當,避免過多的栽程影響性能。
3.4、並行回收集器(ParallelGC)
老年代ParallelOldGC回收器也是一種多線程的回收器,和新生代的ParallelGC回收器一樣,也是一種關往吞吐量的回收器,他使用了標記壓縮算法進行實現。
-XX:+UseParallelOldGC 進行設置
-XX:+ParallelCThread也可以設置垃圾收集時的線程教量。
3.5、並CMS(並發GC)收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基於“標記-清除”算法實現的,整個收集過程大致分為4個步驟:
①.初始標記(CMS initial mark)
②.並發標記(CMS concurrenr mark)
③.重新標記(CMS remark)
④.並發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟任然需要停頓其他用戶線程。初始標記僅僅只是標記出GC ROOTS能直接關聯到的對象,速度很快,並發標記階段是進行GC ROOTS 根搜索算法階段,會判定對象是否存活。而重新標記階段則是為了修正並發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間會被初始標記階段稍長,但比並發標記階段要短。
由於整個過程中耗時最長的並發標記和並發清除過程中,收集器線程都可以與用戶線程一起工作,所以整體來說,CMS收集器的內存回收過程是與用戶線程一起並發執行的。
CMS收集器的優點:並發收集、低停頓
但是CMS還遠遠達不到完美,主要有三個顯著缺點:
CMS收集器對CPU資源非常敏感。在並發階段,雖然不會導致用戶線程停頓,但是會占用CPU資源而導致引用程序變慢,總吞吐量下降。CMS默認啟動的回收線程數是:(CPU數量+3) / 4。
CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗后而導致另一次Full GC的產生。由於CMS並發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱為“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,即需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分內存空間提供並發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以降低內存回收次數提高性能。要是CMS運行期間預留的內存無法滿足程序其他線程需要,就會出現“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置的過高將會很容易導致“Concurrent Mode Failure”失敗,性能反而降低。
CMS是基於“標記-清除”算法實現的收集器,使用“標記-清除”算法收集后,會產生大量碎片。空間碎片太多時,將會給對象分配帶來很多麻煩,比如說大對象,內存空間找不到連續的空間來分配不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用於在Full GC之后增加一個碎片整理過程,還可通過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full GC之后,跟着來一次碎片整理過程。
3.6、G1回收器
G1是一個分代分步的並行收集器,並且他也會有stop-the-world問題。與其他收集器類似,G1將堆內存分為新生代和老年代。內存回收主要發生在新生代,而在老年代中也會間歇性回收內存。
為了提高吞吐量G1將一些指定操作只在stw階段進行,例如全局標記這樣耗時間的操作會和用戶的應用一起並發進行。為了降低垃圾回收時的stw暫停時長,G1通過並行分步的方式進行垃圾回收。G1通過對應用和垃圾回收的行為記錄進行建模,從而達到可預測的回收停頓時間,並且通過這些數據來衡量暫停時候完成的工作量。舉例來說,G1首先在回收效率最高的區域(垃圾最多的區域)進行回收,G1通過分散法來回收空間:將存活對象復制分散到新的內存區域,並且在此過程中進行對象整理。在此之后,之前被占據的那一片區域都會釋放出來。
G1收集器其實並非實時垃圾回收器。他的目標是在長期運行的應用中達到設置的目標暫停時間。
使用.XXX:+UseG1GC 應用G1收集器,
Mills指定最大停頓時間
使用-XX:MaxGCPausel
設置並行回收的線程數量
使用-XX:ParallelGCThreads