Java虛擬機垃圾回收機制

Java語言從出現到如今,一直佔據編程語言前列,他很大的一個緣由就是因爲java應用程序所運行的平臺有關。咱們你們都知道java應用程序運行在java虛擬機上。這樣就大大減小了java應用程序和底層操做系統打交道的頻率。這也就爲java程序的跨平臺提供了良好的基礎。在java虛擬機中爲咱們提供了一個很重要的機制就是java虛擬機的自動的內存管理機制。也就是咱們平時所說的垃圾回收機制,這使得開發人員不用本身來管理應用中的內存。C/C++開發人員須要經過malloc/free 和new/delete等函數來顯式的分配和釋放內存。這對開發人員提出了比較高的要求,容易形成內存訪問錯誤和內存泄露等問題。今天咱們就一塊兒來看一下java虛擬機給咱們提供的這個強大的功能——自動垃圾回收機制。java

咱們在c/c++的程序中,他們沒有java中的自動垃圾回收機制,這就須要開發人員手動的去分配和釋放內存,這樣就要求咱們的開發人員要有必定的細心和對內存管理的經驗。若是內存管理很差,很容易產生最多見的兩個問題。一是「懸掛引用」,二是內存溢出。所爲的懸掛引用就是一個對象引用所指向的內存區塊已經被錯誤的回收並從新分配給新的對象了,程序若是繼續使用這個引用的話會形成不可預期的結果。第二個內存溢出就很好理解了,開發人員在作開發的過程當中,只顯示的申請內存而忘記用完釋放掉內存,這樣長時間會致使內存溢出的狀況。而像java這種具備自動管理內存機制的語言來講,咱們開發人員只需考慮引用的運用就能夠,把內存管理這塊交給咱們的語言運行環境來管理。。開發人員並不須要關心內存的分配和回收的底層細節。Java平臺經過垃圾回收器來進行自動的內存管理。這樣就大大減小了開發人員的工做量c++


1、Java垃圾回收機制算法

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

1.分配內存緩存

2.確保被引用的對象的內存不被錯誤回收服務器

3.回收再也不被引用的對象的內存空間。多線程

垃圾回收是一個複雜並且耗時的操做。若是JVM 花費過多的時間在垃圾回收上,則勢必會影響應用的運行性能。通常狀況下,當垃圾回收器在進行回收操做的時候,整個應用的執行是被暫時停止(stop-the-world)的。這是由於垃圾回收器須要更新應用中全部對象引用的實際內存地址。不一樣的硬件平臺所能支持的垃圾回收方式也不一樣。好比在多CPU 的平臺上,就能夠經過並行的方式來回收垃圾。而單CPU 平臺則只能串行進行。不一樣的應用所指望的垃圾回收方式也會有所不一樣。服務器端應用可能但願在應用的整個運行時間中,花在垃圾回收上的時間總數越小越好。而對於與用戶交互的應用來講,則可能但願所垃圾回收所帶來的應用停頓的時間間隔越小越好。對於這種狀況,JVM 中提供了多種垃圾回收方法以及對應的性能調優參數,應用能夠根據須要來進行定製。併發


2、判斷對象是否該被回收算法框架

1.引用計數算法編程語言

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1;任什麼時候刻計數器值都爲0時對象就表示它不可能被使用了。這個算法實現簡單,但很難解決對象之間循環引用的問題,所以Java並無用這種算法!這是不少人都誤解了的地方。

2.根搜索算法

經過一系列名爲「GC ROOT」的對象做爲起始點,從這些結點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC ROOT沒有任何引用鏈相連時,則證實這個對象是不可用的。若是對象在進行根搜索後發現沒有與GC ROOT相鏈接的引用鏈,則會被第一次第標記,並看此對象是否須要執行finalize()方法(忘記finalize()這個方法吧,它能夠被try-finally或其餘方式代替的),當第二次被標記時,對象就會被回收。


3、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。

在對垃圾收集算法進行評價時,咱們可能要考慮如下全部標準:

1)暫停時間。收集器是否中止全部工做來進行垃圾收集?要中止多長時間?暫停是否有時間限制? 

2)暫停的可預測性。垃圾收集暫停是否規劃爲在用戶程序方便而不是垃圾收集器方便的時間發生? 

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

4)內存大小。許多垃圾收集算法須要將堆分割成獨立的內存空間,其中一些空間在某些時刻對用戶程序是不可訪問的。這意味着堆的實際大小可能比用戶程序的最大堆駐留空間要大幾倍。 

5)虛擬內存交互。在具備有限物理內存的系統上,一個完整的垃圾收集在垃圾收集過程當中可能會錯誤地將很是駐頁面放到內存中來進行檢查。由於頁面錯誤的成本很高,因此垃圾收集器正確管理引用的區域性 (locality) 是很必要的。 

6)緩存交互。即便在整個堆能夠放到主內存中的系統上 ―― 實際上幾乎全部 Java 應用程序均可以作到這一點,垃圾收集也經常會有將用戶程序使用的數據衝出緩存的效果,從而影響用戶程序的性能。 

7)對程序區域性的影響。雖然一些人認爲垃圾收集器的工做只是收回不可到達的內存,可是其餘人認爲垃圾收集器還應該儘可能改進用戶程序的引用區域性。整理收集器和複製收集器在收集過程當中從新安排對象,這有可能改進區域性。 

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

無論選擇什麼算法,硬件和軟件的發展使垃圾收集更具備實用性。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>指定還有多少剩餘堆時開始執行併發收集。


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

通過上述的說明,能夠發現垃圾回收有如下的幾個特色:

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

2)垃圾收集的精確性:主要包括2 個方面:

(a)垃圾收集器可以精確標記活着的對象;

(b)垃圾收集器可以精確地定位對象之間的引用關係。前者是徹底地回收全部廢棄對象的前提,不然就可能形成內存泄漏。然後者則是實現歸併和複製等算法的必要條件。全部不可達對象都可以可靠地獲得回收,全部對象都可以從新分配,容許對象的複製和對象內存的縮並,這樣就有效地防止內存的支離破碎。

3)如今有許多種不一樣的垃圾收集器,每種有其算法且其表現各異,既有當垃圾收集開始時就中止應用程序的運行,又有當垃圾收集開始時也容許應用程序的線程運行,還有在同一時間垃圾收集多線程運行。

4)垃圾收集的實現和具體的JVM 以及JVM的內存模型有很是緊密的關係。不一樣的JVM 可能採用不一樣的垃圾收集,而JVM 的內存模型決定着該JVM能夠採用哪些類型垃圾收集。如今,HotSpot 系列JVM中的內存系統都採用先進的面向對象的框架設計,這使得該系列JVM均可以採用最早進的垃圾收集。

5)隨着技術的發展,現代垃圾收集技術提供許多可選的垃圾收集器,並且在配置每種收集器的時候又能夠設置不一樣的參數,這就使得根據不一樣的應用環境得到最優的應用性能成爲可能。

相關文章
相關標籤/搜索