Java的一大特性就是內存的分配和回收都是自動進行的。當程序規模不大時,咱們徹底能夠不考慮內存的使用狀況。可是一旦程序的規模足夠大,對性能的要求足夠高時,瞭解Java垃圾收集(GC)的內部機制並根據具體的應用特徵來調整使用的垃圾收集算法就顯得十分重要了。程序員
吞吐量(Throughput):程序運行時間 /(程序運行時間 + 垃圾收集時間)
延遲(Latency):使程序儘量少的由於垃圾回收而暫停的能力
足跡(Footprint):GC運行時使用的內存空間
敏捷度(Promptness):對象被標記爲死亡到對象所佔內存被回收所經歷的時間算法
堆大小
堆內對象的存活率
內存分配速率
引用更新速率
對象的壽命編程
標記階段,目的是將不可用的對象標記出來,以便進行後階段的回收。那麼,如何判斷一個對象是否可用呢?這跟指向該對象的引用有很大的關係。所以,在具體研究對象可用性斷定算法以前,讓咱們先看一看Java中不一樣的引用類型。安全
Java引用類型服務器
Java中主要有如下四種引用類型:多線程
該算法給每一個對象添加一個引用計數器,當有引用指向對象時,計數器加1,當引用失效時,計數器減1。所以,當一個對象的引用計數變爲0時,就證實該對象不可用,其所佔用的內存也能夠當即被釋放。架構
可是主流的Java虛擬機中並無使用這一簡單高效的算法來管理內存,主要緣由就是它沒法解決循環引用(Circular)的問題。也即,當對象A和對象B相互引用,而沒有任何其餘對象指向A和B時,因爲A和B的引用計數均爲1(不等於0),引用計數算法將沒法回收這兩個對象。併發
同時,該算法對引用計數的頻繁更新也會使得效率下降。jvm
從一系列名爲」GC Roots」的對象開始向下搜索,就能夠造成若干條引用鏈。若是一個對象到」GC Roots」無任何引用鏈相連,該對象則被斷定爲可回收對象。工具
能夠做爲GC Roots的對象包括如下幾類:
清理階段。將Mark階段標記出的不可用對象清除,釋放其所佔用的內存空間。主要有如下幾種實現方式。
算法思想:遍歷堆空間,將Mark階段標記不可用的對象清除。
不足: 效率不高;空間問題,屢次清除以後會產生大量的內存碎片。
適用場景:對象壽命長的內存區域。
該算法過程以下圖所示:
算法思想:將內存劃分爲兩個區域(大小比例可調整),每次只用其中一塊,當此塊內存用完時,就將存活對象複製到另外一塊內存中,並對當前塊進行內存回收。
優勢:解決了內存碎片問題;內存分配效率提升。每次複製後對象在堆中都是線性排列的,所以內存分配時只需移動堆頂指針便可。
不足:若是對象的存活率較高,大量的複製操做會顯著的下降效率;內存空間浪費,每次都只能使用堆空間的一部分,代價高昂。
該算法過程以下圖所示:
算法思想:將標記的全部可用對象向內存一端移動,而後直接清理邊界之外的內存區域便可。
優勢:相似於複製算法,解決了內存碎片問題,內存分配效率提升;消除了複製算法對內存空間的浪費。
不足:難以作到並行。
該算法過程以下圖所示:
前面所述的Mark-Clean算法都是針對整個堆區域的,每一次GC運行都須要對堆中全部的對象進行遍歷。所以,隨着堆中對象數量的增多,GC的效率就會隨之降低。因而,GC對程序運行作出以下假設:
基於這兩個假設,GC將堆中的對象按照存活時間分爲三代:Young(新生代)、Old(老年代)、Perm(永久代)。其內存劃分示意圖以下:
由圖可見,新生代又可劃分爲三個區域:Eden,Survivor0,Survivor1。其中,Eden區最大,新對象的內存分配都在此區域進行。兩個Survivor區域一個爲From區,一個爲To區,每次只使用其中的一個。
新生代的垃圾回收採用的是複製算法。第一次GC時,Eden區的存活對象會被複制到S0區。此後每次進行GC時,Eden區和From區的存活對象都會被複制到To區。若是一個對象在經歷了幾回垃圾回收後仍然存活,那麼它就會被複制到Old Generation(老年代),此過程稱爲Promotion。
老年代的對象是由新生代對象通過Promotion而來,基於前面列出的假設:「若是對象已存活一段時間,那它極可能會繼續存活一段時間」,該區域的對象存活率廣泛較高,所以通常採用Mark-Sweep或Mark-Compact算法。
永久代並不用來存儲從老年代通過Promotion而來的對象,它存儲的是元數據,包括已被虛擬機加載的類信息、常量、靜態變量、方法等。該區域一般不會發生垃圾回收。
在程序執行時,並不是任什麼時候候均可以停下來進行垃圾回收,只有到達某些特定的點時才能暫停,這些點稱爲安全點(Safepoint)。安全點的設定既不能太少以至於讓GC等待時間過長,也不能太頻繁致使運行時負荷增大。通常在方法調用、循環跳轉、異常跳轉處會產生安全點。
那麼,如何在GC發生時讓全部的用戶線程都「跑」到最近的安全點上停下來呢,有如下兩種方案:
一、搶先式中斷:在GC發生時即中斷全部用戶線程,如有的線程中斷的地方不是安全點,則恢復該線程,讓它跑到安全點上再暫停。(幾乎不用)
二、主動式中斷:GC在其要開始運行前設置一個標誌。而每一個用戶線程在運行過程當中都會去主動的輪詢這個標誌,若是標誌爲真則主動中斷掛起。因爲輪詢標誌的地方和安全點重合,所以線程暫停的地方必定是安全的。
可是,以上實現方案有一種狀況沒法解決,那就是用戶線程不運行的時候,也即處於sleep或blocked狀態的時候。因爲此時線程沒法輪詢中斷標誌,也就不能保證GC開始時它必定處於安全狀態。此時就須要引入安全區域(Safe Region)的概念了,它是指在一段代碼片斷中,對象之間的引用關係不會發生變化。安全區域能夠看作是擴展了的安全點。
當用戶線程執行到Safe Region時,首先會標誌本身進入了安全區域。那麼,就算GC要開始時該線程處於blocked狀態,GC也能夠放心的執行垃圾回收動做了。而當線程要離開Safe Region時,要先檢查GC是否已經完成。若是完成了,線程就能夠繼續執行,不然需等待直到收到能夠安全離開Safe Region的信號爲止。
不一樣的虛擬機中一般有不止一種的垃圾收集器,它們實現了不一樣的垃圾收集算法。如下列舉在Sun HotSpot虛擬機中包含的垃圾收集器。
單線程收集器,採用複製算法。GC運行時會暫停全部的用戶線程(STW,Stop The World)。是虛擬機運行在Client模式下的默認新生代收集器。
Serial收集器的多線程版本,除此以外與Serial收集器幾乎徹底相同。是許多運行在Server模式下的虛擬機中首選的新生代收集器。緣由之一是它是惟一能與CMS配合使用的新生代收集器。
可以使用-XX:ParallelGCThreads參數指定垃圾收集的線程數。
與ParNew同樣,是使用複製算法的多線程收集器。可是不一樣於ParNew對縮短垃圾收集時用戶線程停頓時間的關注,Parallel Scavenge更多的是關注提升程序的吞吐量,所以常被稱爲「吞吐量優先」收集器。適用於在後臺運算而沒有太多交互的任務。
Serial收集器的老年代版本,單線程收集器,使用Mark-Compact算法。
主要用於Client模式下的虛擬機。但在Server模式下也有兩大用途。
Parallel Scavenge的老年代版本,使用多線程和Mark-Compact算法,JDK1.6開始提供。
下圖展現了Serial和Parallel收集器的工做模式。
Concurrent Mark Sweep,其工做過程可分爲如下四個步驟:
由圖可見,CMS執行過程當中大部分階段都是與用戶線程並行進行的,所以用戶線程暫停時間會大大減小。可是因爲CMS在進行清理時,用戶線程也在運行,也即此時仍然會有新的垃圾產生。這些垃圾稱爲「浮動垃圾」(Floating Garbage)。因爲「浮動垃圾」產生於CMS標記階段以後,它們只能等到下一次GC時纔可被回收。因此,CMS並不能等到老年代幾乎要滿了纔開始垃圾收集動做,它必須預留足夠的空間給用戶線程在垃圾收集過程當中使用。若是預留的空間預估不許的話,就有可能出現如下兩種狀況:
可以使用-XX:+CMSInitiatingOccupancyFraction參數來指定在老年代空間被使用多少後觸發垃圾收集,默認爲68%。
之因此把G1單獨列出來,是由於它在內存年代劃分上不一樣於上面介紹的全部收集器。G1把內存分爲不少個大小相等的獨立區域(Region),新生代和老年代再也不是相互隔離的,而是都由若干個非連續的Region組成。除此以外,G1收集器還有如下幾個特色:
忽略RSet的維護操做,G1的執行過程主要分爲如下四步,其與CMS的執行過程很類似:
一、初始標記(Initial Mark):STW方式。標記GC Roots直接引用的對象,並修改TAMS的值,使下一階段用戶線程併發運行時可以在正確可用的Region中建立新對象。時間很短。
二、併發標記(Concurrent Mark):進行GC Roots Tracing,標記出全部可用對象。
三、最終標記(Final Mark):將併發標記期間因程序繼續運行而變化的引用合併到RSet中。
四、篩選回收(Live Data Counting and Evacuation):對各個Region按照回收價值和成本進行排序,而後根據用戶所指望的GC停頓時間來制定回收計劃。
其執行過程以下圖所示:
在實際應用中,經常須要根據不一樣的應用特徵調整垃圾收集器的配置方案。在調整過程當中,難免須要監控各類收集器的運行過程來進行性能的比較。JDK自帶了一個Visual VM工具來可視化GC的執行過程。筆者最近爲了跟蹤服務器上不一樣垃圾收集器實現的性能,分析了較多的GC日誌。不一樣收集器生成的日誌格式可能不盡相同,但都有必定的共性。下面列出的是在實際應用中使用ParNew+CMS和使用G1時產生的日誌,從中能夠很清楚的看到CMS和G1的執行階段以及GC運行時用戶線程暫停的時間,有興趣的朋友能夠研究一下
對Java架構技術,對架構技術感興趣的同窗,歡迎加QQqun:859729143 Java點擊進入,一塊兒學習,相互討論。
羣內已經有小夥伴將知識體系整理好(源碼,筆記,PPT,學習視頻),歡迎?加羣免費領取
分享給喜歡Java,喜歡編程,有夢想成爲架構師的程序員們,但願可以幫助到大家。
ParNew+CMS日誌
G1日誌
使用的日誌相關參數以下
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
避免顯式調用System.gc()或Runtime.getRuntime().gc()。這兩個方法只是給虛擬機一個建議,是否執行垃圾回收仍是由虛擬機來決定。 不要在finalize()方法中釋放資源。 不要嘗試在finalize()方法中逃脫垃圾回收。