原創不易,如需轉載,請註明出處http://www.javashuo.com/article/p-oljsybpz-hc.html,多多支持哈!html
GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會致使程序或系統的不穩定甚至崩潰,Java提供的GC功能能夠自動監測對象是否超過做用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操做方法。Java程序員不用擔憂內存管理,由於垃圾收集器會自動進行管理。要請求垃圾收集,能夠調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc()。java
哪些內存須要回收是垃圾回收機制第一個要考慮的問題,所謂「要回收的垃圾」無非就是那些不可能再被任何途徑使用的對象。那麼如何找到這些對象?git
那麼問題又來了,如何選取GCRoots對象呢?在Java語言中,能夠做爲GCRoots的對象包括下面幾種:程序員
下面給出一個GCRoots的例子,以下圖,爲GCRoots的引用鏈,obj八、obj九、obj10都沒有到GCRoots對象的引用鏈,因此會進行回收。github
對於可達性分析算法而言,未到達的對象並不是是「非死不可」的,若要宣判一個對象死亡,至少須要經歷兩次標記階段。算法
對F-Queue中對象進行第二次標記,若是對象在finalize方法中拯救了本身,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其餘變量,那麼在第二次標記的時候該對象將從「即將回收」的集合中移除,若是對象仍是沒有拯救本身,那就會被回收。以下代碼演示了一個對象如何在finalize方法中拯救了本身,然而,它只能拯救本身一次,第二次就被回收了。具體代碼以下:編程
public class GC {segmentfault
public static GC SAVE_HOOK = null; public static void main(String[] args) throws InterruptedException { // 新建對象,由於SAVE_HOOK指向這個對象,對象此時的狀態是(reachable,unfinalized) SAVE_HOOK = new GC(); //將SAVE_HOOK設置成null,此時剛纔建立的對象就不可達了,由於沒有句柄再指向它了,對象此時狀態是(unreachable,unfinalized) SAVE_HOOK = null; //強制系統執行垃圾回收,系統發現剛纔建立的對象處於unreachable狀態,並檢測到這個對象的類覆蓋了finalize方法,所以把這個對象放入F-Queue隊列,由低優先級線程執行它的finalize方法,此時對象的狀態變成(unreachable, finalizable)或者是(finalizer-reachable,finalizable) System.gc(); // sleep,目的是給低優先級線程從F-Queue隊列取出對象並執行其finalize方法提供機會。在執行完對象的finalize方法中的super.finalize()時,對象的狀態變成(unreachable,finalized)狀態,但接下來在finalize方法中又執行了SAVE_HOOK = this;這句話,又有句柄指向這個對象了,對象又可達了。所以對象的狀態又變成了(reachable, finalized)狀態。 Thread.sleep(500); // 這裏樓主說對象處於(reachable,finalized)狀態應該是合理的。對象的finalized方法被執行了,所以是finalized狀態。又由於在finalize方法是執行了SAVE_HOOK=this這句話,原本是unreachable的對象,又變成reachable了。 if (null != SAVE_HOOK) { //此時對象應該處於(reachable, finalized)狀態 // 這句話會輸出,注意對象由unreachable,通過finalize復活了。 System.out.println("Yes , I am still alive"); } else { System.out.println("No , I am dead"); } // 再一次將SAVE_HOOK放空,此時剛纔復活的對象,狀態變成(unreachable,finalized) SAVE_HOOK = null; // 再一次強制系統回收垃圾,此時系統發現對象不可達,雖然覆蓋了finalize方法,但已經執行過了,所以直接回收。 System.gc(); // 爲系統回收垃圾提供機會 Thread.sleep(500); if (null != SAVE_HOOK) { // 這句話不會輸出,由於對象已經完全消失了。 System.out.println("Yes , I am still alive"); } else { System.out.println("No , I am dead"); } } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("execute method finalize()"); // 這句話讓對象的狀態由unreachable變成reachable,就是對象復活 SAVE_HOOK = this; }
}數組
運行結果以下:多線程
leesf null finalize method executed! leesf yes, i am still alive :) no, i am dead : (
由結果可知,該對象拯救了本身一次,第二次沒有拯救成功,由於對象的finalize方法最多被虛擬機調用一次。此外,從結果咱們能夠得知,一個堆對象的this(放在局部變量表中的第一項)引用會永遠存在,在方法體內能夠將this引用賦值給其餘變量,這樣堆中對象就能夠被其餘變量所引用,即不會被回收。
一、方法區的垃圾回收主要回收兩部份內容:
二、既然進行垃圾回收,就須要判斷哪些是廢棄常量,哪些是無用的類?
如何判斷無用的類呢?須要知足如下三個條件:
這是最基礎的算法,標記-清除算法就如同它的名字樣,分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,標記完成後統一回收全部被標記的對象。這種算法的不足主要體如今效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除後會產生大量不連續的內存碎片, 內存碎片太多可能會致使之後程序運行過程當中在須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發一次垃圾收集動做。標記-清除算法執行過程如圖:
複製算法是爲了解決效率問題而出現的,它將可用的內存分爲兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已經使用過的內存空間一次性清理掉。這樣每次只須要對整個半區進行內存回收,內存分配時也不須要考慮內存碎片等複雜狀況,只須要移動指針,按照順序分配便可。複製算法的執行過程如圖:
不過這種算法有個缺點,內存縮小爲了原來的一半,這樣代價過高了。如今的商用虛擬機都採用這種算法來回收新生代,不過研究代表1:1的比例很是不科學,所以新生代的內存被劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活着的對象一次性複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例爲8:1,意思是每次新生代中可用內存空間爲整個新生代容量的90%。固然,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴老年代進行分配擔保(Handle Promotion)。
複製算法在對象存活率較高的場景下要進行大量的複製操做,效率很低。萬一對象100%存活,那麼須要有額外的空間進行分配擔保。老年代都是不易被回收的對象,對象存活率高,所以通常不能直接選用複製算法。根據老年代的特色,有人提出了另一種標記-整理算法,過程與標記-清除算法同樣,不過不是直接對可回收對象進行清理,而是讓全部存活對象都向一端移動,而後直接清理掉邊界之外的內存。標記-整理算法的工做過程如圖:
垃圾收集器就是上面講的理論知識的具體實現了。不一樣虛擬機所提供的垃圾收集器可能會有很大差異,咱們使用的是HotSpot,HotSpot這個虛擬機所包含的全部收集器如圖:
上圖展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,那說明它們能夠搭配使用。虛擬機所處的區域說明它是屬於新生代收集器仍是老年代收集器。多說一句,咱們必須明確一個觀點:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot爲何要實現這麼多收集器的緣由。OK,下面一個一個看一下收集器。
最基本、發展歷史最久的收集器,這個收集器是一個採用複製算法的單線程的收集器,單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工做,另外一方面也意味着它進行垃圾收集時必須暫停其餘線程的全部工做,直到它收集結束爲止。後者意味着,在用戶不可見的狀況下要把用戶正常工做的線程所有停掉,這對不少應用是難以接受的。不過實際上到目前爲止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器,由於它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的內存通常來講不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是徹底能夠接受的。Serial收集器運行過程以下圖所示:
說明:1. 須要STW(Stop The World),停頓時間長。2. 簡單高效,對於單個CPU環境而言,Serial收集器因爲沒有線程交互開銷,能夠獲取最高的單線程收集效率。
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其他行爲和Serial收集器徹底同樣,包括使用的也是複製算法。ParNew收集器除了多線程之外和Serial收集器並無太多創新的地方,可是它倒是Server模式下的虛擬機首選的新生代收集器,其中有一個很重要的和性能無關的緣由是,除了Serial收集器外,目前只有它能與CMS收集器配合工做(看圖)。CMS收集器是一款幾乎能夠認爲有劃時代意義的垃圾收集器,由於它第一次實現了讓垃圾收集線程與用戶線程基本上同時工做。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至因爲線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證能夠超越Serial收集器。固然,隨着可用CPU數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。它默認開啓的收集線程數與CPU數量相同,在CPU數量很是多的狀況下,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。ParNew收集器運行過程以下圖所示:
Parallel Scavenge收集器也是一個新生代收集器,也是用複製算法的收集器,也是並行的多線程收集器,可是它的特色是它的關注點和其餘收集器不一樣。介紹這個收集器主要仍是介紹吞吐量的概念。CMS等收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是打到一個可控制的吞吐量。所謂吞吐量的意思就是CPU用於運行用戶代碼時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總運行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,<font color=red>Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器</font>。
停頓時間短適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗;高吞吐量則能夠高效率利用CPU時間,儘快完成運算任務,主要適合在後臺運算而不須要太多交互的任務。
虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要覺得前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。因爲與吞吐量關係密切,Parallel Scavenge收集器也被稱爲「吞吐量優先收集器」。<font color=red>Parallel Scavenge收集器有一個-XX:+UseAdaptiveSizePolicy參數,這是一個開關參數,這個參數打開以後,就不須要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當前系統的運行狀況以及性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。</font>若是對於垃圾收集器運做原理不太瞭解,以致於在優化比較困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇。
Serial收集器的老年代版本,一樣是一個單線程收集器,使用「標記-整理算法」,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。
Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器在JDK 1.6以後的出現,「吞吐量優先收集器」終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合。運行過程以下圖所示:
<font color=red>CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時間爲目標的收集器</font>。使用標記 - 清除算法,收集過程分爲以下四步:
其中,併發標記與併發清除兩個階段耗時最長,可是能夠與用戶線程併發執行。運行過程以下圖所示:
說明:
G1算法將堆劃分爲若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停全部應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分紅不少區域,G1收集器經過將對象從一個區域複製到另一個區域,完成了清理工做。這就意味着,在正常的處理過程當中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。
在G1中,還有一種特殊的區域,叫Humongous區域。 若是一個對象佔用的空間超過了分區容量50%以上,G1收集器就認爲這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,可是若是它是一個短時間存在的巨型對象,就會對垃圾收集器形成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。若是一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC。
G1主要有如下特色:
在G1以前的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,<font color=red>Java堆的內存佈局與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分(能夠不連續)Region的集合</font>。
<font color="red">注意:這些space必須是地址連續的空間</font>
對象分配
優先在Eden區分配
在JVM內存模型一文中, 咱們大體瞭解了VM年輕代堆內存能夠劃分爲一塊Eden區和兩塊Survivor區. 在大多數狀況下, 對象在新生代Eden區中分配, 當Eden區沒有足夠空間分配時, VM發起一次Minor GC, 將Eden區和其中一塊Survivor區內尚存活的對象放入另外一塊Survivor區域, 若是在Minor GC期間發現新生代存活對象沒法放入空閒的Survivor區, 則會經過空間分配擔保機制使對象提早進入老年代(空間分配擔保見下).
大對象直接進入老年代
Serial和ParNew兩款收集器提供了-XX:PretenureSizeThreshold的參數, 令大於該值的大對象直接在老年代分配, 這樣作的目的是避免在Eden區和Survivor區之間產生大量的內存複製(大對象通常指 須要大量連續內存的Java對象, 如很長的字符串和數組), 所以大對象容易致使還有很多空閒內存就提早觸發GC以獲取足夠的連續空間. 然而取歷次晉升的對象的平均大小也是有必定風險的, 若是某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然可能致使擔保失敗(Handle Promotion Failure, 老年代也沒法存放這些對象了), 此時就只好在失敗後從新發起一次Full GC(讓老年代騰出更多空間).
空間分配擔保
在執行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 因爲新生代使用複製收集算法, 爲了提高內存利用率, 只使用了其中一個Survivor做爲輪換備份, 所以當出現大量對象在Minor GC後仍然存活的狀況時, 就須要老年代進行分配擔保, 讓Survivor沒法容納的對象直接進入老年代, 但前提是老年代須要有足夠的空間容納這些存活對象. 但存活對象的大小在實際完成GC前是沒法明確知道的, 所以Minor GC前, VM會先首先檢查老年代連續空間是否大於新生代對象總大小或歷次晉升的平均大小, 若是條件成立, 則進行Minor GC, 不然進行Full GC(讓老年代騰出更多空間).
對象晉升
年齡閾值
VM爲每一個對象定義了一個對象年齡(Age)計數器, 對象在Eden出生若是經第一次Minor GC後仍然存活, 且能被Survivor容納的話, 將被移動到Survivor空間中, 並將年齡設爲1. 之後對象在Survivor區中每熬過一次Minor GC年齡就+1. 當增長到必定程度(-XX:MaxTenuringThreshold, 默認15), 將會晉升到老年代.
提早晉升: 動態年齡斷定
然而VM並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代: 若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就能夠直接進入老年代, 而無須等到晉升年齡.
發生在年輕代的GC算法,通常對象(除了巨型對象)都是在eden region中分配內存,當全部eden region被耗盡沒法申請內存時,就會觸發一次young gc,這種觸發機制和以前的young gc差很少,執行完一次young gc,活躍對象會被拷貝到survivor region或者晉升到old region中,空閒的region會被放入空閒列表中,等待下次被使用。
當愈來愈多的對象晉升到老年代old region時,爲了不堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即mixed gc,該算法並非一個old gc,除了回收整個young region,還會回收一部分的old region,這裏須要注意:是一部分老年代,而不是所有老年代,能夠選擇哪些old region進行收集,從而能夠對垃圾回收的耗時時間進行控制。
若是對象內存分配速度過快,mixed gc來不及回收,致使老年代被填滿,就會觸發一次full gc,G1的full gc算法就是單線程執行的serial old gc,會致使異常長時間的暫停時間,須要進行不斷的調優,儘量的避免full gc.
首先查看你使用的垃圾回收器是什麼?
java -XX:+PrintCommandLineFlags -version
我的博客地址:
csdn: https://blog.csdn.net/tiantuo6513cnblogs:https://www.cnblogs.com/baixianlong
segmentfault:https://segmentfault.com/u/baixianlong
本文參考:
<font color="gray">https://www.cnblogs.com/xiaox...;/font>
<font color="gray">https://zhuanlan.zhihu.com/p/...;/font>