1. 程式人生 > >Java程式設計師從笨鳥到菜鳥之(九十五)深入java虛擬機器(四)——java虛擬機器的垃圾回收機制

Java程式設計師從笨鳥到菜鳥之(九十五)深入java虛擬機器(四)——java虛擬機器的垃圾回收機制

         Java語言從出現到現在,一直佔據程式語言前列,他很大的一個原因就是由於java應用程式所執行的平臺有關。我們大家都知道java應用程式執行在java虛擬機器上。這樣就大大減少了java應用程式和底層作業系統打交道的頻率。這也就為java程式的跨平臺提供了良好的基礎。在java虛擬機器中為我們提供了一個很重要的機制就是java虛擬機器的自動的記憶體管理機制。也就是我們平時所說的垃圾回收機制,這使得開發人員不用自己來管理應用中的記憶體。C/C++開發人員需要通過malloc/free 和new/delete等函式來顯式的分配和釋放記憶體。這對開發人員提出了比較高的要求,容易造成記憶體訪問錯誤和記憶體洩露等問題。今天我們就一起來看一下java

虛擬機器給我們提供的這個強大的功能——自動垃圾回收機制。

      我們在c/c++的程式中,他們沒有java中的自動垃圾回收機制,這就需要開發人員手動的去分配和釋放記憶體,這樣就要求我們的開發人員要有一定的細心和對記憶體管理的經驗。如果記憶體管理不好,很容易產生最常見的兩個問題。一是“懸掛引用”,二是記憶體溢位。所為的懸掛引用就是一個物件引用所指向的記憶體區塊已經被錯誤的回收並重新分配給新的物件了,程式如果繼續使用這個引用的話會造成不可預期的結果。第二個記憶體溢位就很好理解了,開發人員在做開發的過程中,只顯示的申請記憶體而忘記用完釋放掉記憶體,這樣長時間會導致記憶體溢位的情況。而像java這種具有自動管理記憶體機制的語言來說,我們開發人員只需考慮引用的運用就可以,把記憶體管理這塊交給我們的語言執行環境來管理。。開發人員並不需要關心記憶體的分配和回收的底層細節。Java平臺通過垃圾回收器來進行自動的記憶體管理。這樣就大大減少了開發人員的工作量

一、Java垃圾回收機制

Java 的垃圾回收器要負責完成3 件任務:

1.分配記憶體

2.確保被引用的物件的記憶體不被錯誤回收

3.回收不再被引用的物件的記憶體空間。

        垃圾回收是一個複雜而且耗時的操作。如果JVM 花費過多的時間在垃圾回收上,則勢必會影響應用的執行效能。一般情況下,當垃圾回收器在進行回收操作的時候,整個應用的執行是被暫時中止(stop-the-world)的。這是因為垃圾回收器需要更新應用中所有物件引用的實際記憶體地址。不同的硬體平臺所能支援的垃圾回收方式也不同。比如在多CPU 的平臺上,就可以通過並行的方式來回收垃圾。而單CPU 平臺則只能序列進行。不同的應用所期望的垃圾回收方式也會有所不同。伺服器端應用可能希望在應用的整個執行時間中,花在垃圾回收上的時間總數越小越好。而對於與使用者互動的應用來說,則可能希望所垃圾回收所帶來的應用停頓的時間間隔越小越好。對於這種情況,JVM 中提供了多種垃圾回收方法以及對應的效能調優引數,應用可以根據需要來進行定製。

二、判斷物件是否該被回收演算法

1.引用計數演算法

給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1;任何時刻計數器值都為0時物件就表示它不可能被使用了。這個演算法實現簡單,但很難解決物件之間迴圈引用的問題,因此Java並沒有用這種演算法!這是很多人都誤解了的地方。

2.根搜尋演算法

通過一系列名為“GC ROOT”的物件作為起始點,從這些結點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC ROOT沒有任何引用鏈相連時,則證明這個物件是不可用的。如果物件在進行根搜尋後發現沒有與GC ROOT相連線的引用鏈,則會被第一次第標記,並看此物件是否需要執行finalize()方法(忘記finalize()這個方法吧,它可以被try-finally或其他方式代替的),當第二次被標記時,物件就會被回收。

三、Java虛擬機器基本垃圾回收演算法

1.標記-清除(Mark-Sweep)

此演算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。它停止所有工作,收集器從根開始訪問每一個活躍的節點,標記它所訪問的每一個節點。走過所有引用後,收集就完成了,然後就對堆進行清除(即對堆中的每一個物件進行檢查),所有沒有標記的物件都作為垃圾回收並返回空閒列表。下圖 展示了垃圾收集之前的堆,陰影塊是垃圾,因為使用者程式不能到達它們:

可到達和不可到達的物件 

                                       
 

標記-清除實現起來很簡單,可以容易地回收迴圈的結構,並且不像引用計數那樣增加編譯器或者賦值函式的負擔。但是它也有不足 ―― 收集暫停可能會很長,在清除階段整個堆都是可訪問的,這對於可能有頁面交換的堆的虛擬記憶體系統有非常負面的效能影響。

標記-清除的最大問題是,每一個活躍的(即已分配的)物件,不管是不是可到達的,在清除階段都是可以訪問的。因為很多物件都可能成為垃圾,這意思著收集器花費大量精力去檢查並處理垃圾。標記-清除收集器還容易使堆產生碎片,這會產生區域性問題並可以造成分配失敗,即使看來有足夠的自由記憶體可用。此演算法需要暫停整個應用,同時,會產生記憶體碎片。

                                    

2.複製(Copying)

此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。次演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不過出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。

                                      

3.標記-整理(Mark-Compact)

此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。

                                  

4.增量收集(Incremental Collecting)

實施垃圾回收演算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種演算法的。

5.分代(Generational Collecting)

將堆分成新生代(Eden, From Survivor, To Survivor)和老年代,在新生代中使用複製演算法,即Minor-GC,當一些物件經過多次的Minor-GC後還留在新生代,則會被搬移到老年代中。而老年代中使用標記-清理或標記-整理演算法,即Major GC/Full GC。

-XX:PretenurseSizeThreshold=1024,則大於次引數的物件會直接分配到老年代(儘可能不要寫一些“短命大物件”!)

-XX:MaxTenuringThreshold=15,在survivor空間存活15次之後,則會搬移到老年代

如果是Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。

進行Minor GC時,虛擬機器會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果大於,則直接進行一次Full GC。

在對垃圾收集演算法進行評價時,我們可能要考慮以下所有標準:

· 暫停時間。收集器是否停止所有工作來進行垃圾收集?要停止多長時間?暫停是否有時間限制? 

· 暫停的可預測性。垃圾收集暫停是否規劃為在使用者程式方便而不是垃圾收集器方便的時間發生? 

· CPU 佔用。總的可用 CPU 時間用在垃圾收集上的百分比是多少? 

· 記憶體大小。許多垃圾收集演算法需要將堆分割成獨立的記憶體空間,其中一些空間在某些時刻對使用者程式是不可訪問的。這意味著堆的實際大小可能比使用者程式的最大堆駐留空間要大幾倍。 

· 虛擬記憶體互動。在具有有限實體記憶體的系統上,一個完整的垃圾收集在垃圾收集過程中可能會錯誤地將非常駐頁面放到記憶體中來進行檢查。因為頁面錯誤的成本很高,所以垃圾收集器正確管理引用的區域性 (locality) 是很必要的。 

· 快取互動。即使在整個堆可以放到主記憶體中的系統上 ―― 實際上幾乎所有 Java 應用程式都可以做到這一點,垃圾收集也常常會有將使用者程式使用的資料衝出快取的效果,從而影響使用者程式的效能。 

· 對程式區域性的影響。雖然一些人認為垃圾收集器的工作只是收回不可到達的記憶體,但是其他人認為垃圾收集器還應該儘量改進使用者程式的引用區域性。整理收集器和複製收集器在收集過程中重新安排物件,這有可能改進區域性。 

· 編譯器和執行時影響。一些垃圾收集演算法要求編譯器或者執行時環境的重要配合,如當進行指標分配時更新引用計數。這增加了編譯器的工作,因為它必須生成這些簿記指令,同時增加了執行時環境的開銷,因為它必須執行這些額外的指令。這些要求對效能有什麼影響呢?它是否會干擾編譯時優化呢?

          不管選擇什麼演算法,硬體和軟體的發展使垃圾收集更具有實用性。20 世紀 70 和 80 年代的經驗研究表明,對於大型 Lisp 程式,垃圾收集消耗 25% 到 40% 的執行時。垃圾收集還不能做到完全不可見,這肯定還有很長的路要走。

三種垃圾回收器

目前的收集器主要有三種:序列收集器、並行收集器、併發收集器。

1.序列收集器

使用單執行緒處理所有垃圾回收工作,因為無需多執行緒互動,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。可以使用-XX:+UseSerialGC開啟。

2.並行收集器 

1對年輕代進行並行垃圾回收,因此可以減少垃圾回收時間。一般在多執行緒多處理器機器上使用。使用-XX:+UseParallelGC.開啟。並行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中進行了增強--可以堆年老代進行並行收集。如果年老代不使用併發收集的話,是使用單執行緒進行垃圾回收,因此會制約擴充套件能力。使用-XX:+UseParallelOldGC開啟。

2使用-XX:ParallelGCThreads=<N>設定並行垃圾回收的執行緒數。此值可以設定與機器處理器數量相等。

3此收集器可以進行如下配置:

最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,通過-XX:MaxGCPauseMillis=<N>指定。<N>為毫秒.如果指定了此值的話,堆大小和垃圾回收相關引數會進行調整以達到指定值。設定此值可能會減少應用的吞吐量。

  吞吐量:吞吐量為垃圾回收時間與非垃圾回收時間的比值,通過-XX:GCTimeRatio=<N>來設定,公式為1/1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。預設情況為99,即1%的時間用於垃圾回收。

3.併發收集器

可以保證大部分工作都併發進行(應用不停止),垃圾回收只暫停很少的時間,此收集器適合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC開啟。

1併發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收執行緒,跟蹤可達物件。在每個年老代垃圾回收週期中,在收集初期併發收集器會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程中多個執行緒同時進行垃圾回收工作。

2併發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,併發收集部分使用K/N個可用處理器進行回收,一般情況下1<=K<=N/4。

3在只有一個處理器的主機上使用併發收集器,設定為incremental mode模式也可獲得較短的停頓時間。

4浮動垃圾:由於在應用執行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了Floating Garbage,這些垃圾需要在下次垃圾回收週期時才能回收掉。所以,併發收集器一般需要20%的預留空間用於這些浮動垃圾。

5Concurrent Mode Failure:併發收集器在應用執行時進行收集,所以需要保證堆在垃圾回收的這段時間有足夠的空間供程式使用,否則,垃圾回收還未完成,堆空間先滿了。這種情況下將會發生併發模式失敗,此時整個應用將會暫停,進行垃圾回收。

6啟動併發收集器:因為併發收集在應用執行時進行收集,所以必須保證收集完成之前有足夠的記憶體空間供程式使用,否則會出現Concurrent Mode Failure。通過設定-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少剩餘堆時開始執行併發收集

五、關於垃圾收集的幾點補充

經過上述的說明,可以發現垃圾回收有以下的幾個特點:

(1)垃圾收集發生的不可預知性:由於實現了不同的垃圾收集演算法和採用了不同的收集機制,所以它有可能是定時發生,有可能是當出現系統空閒CPU資源時發生,也有可能是和原始的垃圾收集一樣,等到記憶體消耗出現極限時發生,這與垃圾收集器的選擇和具體的設定都有關係。

(2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器能夠精確標記活著的物件;(b)垃圾收集器能夠精確地定位物件之間的引用關係。前者是完全地回收所有廢棄物件的前提,否則就可能造成記憶體洩漏。而後者則是實現歸併和複製等演算法的必要條件。所有不可達物件都能夠可靠地得到回收,所有物件都能夠重新分配,允許物件的複製和物件記憶體的縮並,這樣就有效地防止記憶體的支離破碎。

(3)現在有許多種不同的垃圾收集器,每種有其演算法且其表現各異,既有當垃圾收集開始時就停止應用程式的執行,又有當垃圾收集開始時也允許應用程式的執行緒執行,還有在同一時間垃圾收集多執行緒執行。

(4)垃圾收集的實現和具體的JVM 以及JVM的記憶體模型有非常緊密的關係。不同的JVM 可能採用不同的垃圾收集,而JVM 的記憶體模型決定著該JVM可以採用哪些型別垃圾收集。現在,HotSpot 系列JVM中的記憶體系統都採用先進的面向物件的框架設計,這使得該系列JVM都可以採用最先進的垃圾收集。

(5)隨著技術的發展,現代垃圾收集技術提供許多可選的垃圾收集器,而且在配置每種收集器的時候又可以設定不同的引數,這就使得根據不同的應用環境獲得最優的應用效能成為可能。


參考文獻