在1960年誕生於MIT的Lisp語言首次使用了動態內存分配和垃圾收集技術,能夠實現垃圾回收的一個基本要求是語言是類型安全的,如今使用的包括Java、Perl、ML等。java
一、當須要排查各類內存溢出、內存泄漏問題時;
程序員
二、當垃圾收集成爲系統達到更高併發量的瓶頸時;算法
咱們就須要對這些"自動化"技術實話必要的監控和調節;
數組
一、哪些內存須要回收?即如何判斷對象已經死亡;緩存
二、何時回收?即GC發生在何時?須要瞭解GC策略,與垃圾回收器實現有關;安全
三、如何回收?即須要瞭解垃圾回收算法,及算法的實現--垃圾回收器bash
下面先來了解兩種判斷對象再也不被引用的算法,再來談談對象的引用,最後來看如何真正宣告一個對象死亡。服務器
(A)、很難解決對象之間相互循環引用的問題數據結構
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;複製代碼
當兩個對象再也不被訪問時,由於相互引用對方,致使引用計數不爲0;
多線程
更復雜的循環數據結構,如圖:
(B)、而且開銷較大,頻繁且大量的引用變化,帶來大量的額外運算;
主流的JVM都沒有選用引用計數算法來管理內存;
當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到這個對象不可達),則證實該對象是不可用的;
二、GC Roots對象(1)虛擬機棧(棧幀中本地變量表)中引用的對象;
(2)方法區中類靜態屬性引用的對象;
(3)方法區中常量引用的對象;
(4)本地方法棧中JNI(Native方法)引用的對象;
主要在執行上下文中和全局性的引用;
三、優勢後面會針對HotSpot虛擬機實現的可達性分析算法進行介紹,看看是它如何解決這些缺點的。
JVM規範規定reference類型來表示對某個對象的引用,能夠想象成相似於一個指向對象的指針;
對象的操做、傳遞和檢查都經過引用它的reference類型的數據進行操做;若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用;這種定義太過狹隘,沒法描述更多信息;
(ii)、JDK1.2後,對引用概念進行了擴充,將引用分爲:(1)強引用(Strong Reference)
程序代碼廣泛存在的,相似"Object obj=new Object()";
只要強引用還存在,GC永遠不會回收被引用的對象;(2)軟引用(Soft Reference)
用來描述還有用但並不是必需的對象;
直到內存空間不夠時(拋出OutOfMemoryError以前),纔會被垃圾回收;最經常使用於實現對內存敏感的緩存;SoftReference類實現;
(3)弱引用(Weak Reference)
用來描述非必需對象;只能生存到下一次垃圾回收以前,不管內存是否足夠;WeakReference類實現;
(4)虛引用(Phantom Reference)
也稱爲幽靈引用或幻影引用;徹底不會對其生存時間構成影響;惟一目的就是能在這個對象被回收時收到一個系統通知;PhantomRenference類實現;
(A)沒有必要執行
沒有必要執行的狀況:
(1) 對象沒有覆蓋finalize()方法;
(2) finalize()方法已經被JVM調用過;
這兩種狀況就能夠認爲對象已死,能夠回收;
(B) 有必要執行
對有必要執行finalize()方法的對象,被放入F-Queue隊列中;
稍後在JVM自動創建、低優先級的Finalizer線程(可能多個線程)中觸發這個方法;
二、第二次標記GC將對F-Queue隊列中的對象進行第二次小規模標記;
finalize()方法是對象逃脫死亡的最後一次機會:
(A)、若是對象在其finalize()方法中從新與引用鏈上任何一個對象創建關聯,第二次標記時會將其移出"即將回收"的集合;
(B)、若是對象沒有,也能夠認爲對象已死,能夠回收了;
一個對象的finalize()方法只會被系統自動調用一次,通過finalize()方法逃脫死亡的對象,第二次不會再調用;
finalize()是Object類的一個方法,是Java剛誕生時爲了使C/C++程序員容易接受它所作出的一個妥協,但不要看成相似C/C++的析構函數;
由於它執行的時間不肯定,甚至是否被執行也不肯定(Java程序的不正常退出),並且運行代價高昂,沒法保證各個對象的調用順序(甚至有不一樣線程中調用);一、充當"安全網"
當顯式的終止方法沒有調用時,在finalize()方法中發現後發出警告;
但要考慮是否值得付出這樣的代價;如FileInputStream、FileOutputStream、Timer和Connection類中都有這種應用;
二、與對象的本地對等體有關
本地對等體:普通對象調用本地方法(JNI)委託的本地對象;本地對等體不會被GC回收;
若是本地對等體不擁有關鍵資源,finalize()方法裏能夠回收它(如C/C++中malloc(),須要調用free());
若是有關鍵資源,必須顯式的終止方法;
通常狀況下,應儘可能避免使用它,甚至能夠忘掉它。前面對可達性分析算法進行介紹,並看到了它在判斷對象存活與死亡的做用,下面看看是HotSpot虛擬機是如何實現可達性分析算法,如何解決相關缺點的。
一、消耗大量時間
從前面可達性分析知道,GC Roots主要在全局性的引用(常量或靜態屬性)和執行上下文中(棧幀中的本地變量表);是JVM在後臺自動發起和自動完成的;在用戶不可見的狀況下,把用戶正常的工做線程所有停掉;
在類加載時,計算對象內什麼偏移量上是什麼類型的數據;
在JIT編譯時,也會記錄棧和寄存器中的哪些位置是引用;
這樣GC掃描時就能夠直接得知這些信息;
運行中,很是多的指令都會致使引用關係變化;若是爲這些指令都生成對應的OopMap,須要的空間成本過高;
問題解決:只在特定的位置記錄OopMap引用關係,這些位置稱爲安全點(Safepoint);
即程序執行時並不是全部地方都能停頓下來開始GC;
二、安全點的選定只有具備這些功能的指令纔會產生Safepoint;
三、如何在安全點上停頓(A)搶先式中斷(Preemptive Suspension)
不須要線程主動配合,實現以下:
(1)在GC發生時,首先中斷全部線程;
(2)若是發現不在Safepoint上的線程,就恢復讓其運行到Safepoint上;如今幾乎沒有JVM實現採用這種方式;
(B)主動式中斷(Voluntary Suspension)
(1)在GC發生時,不直接操做線程中斷,而是僅簡單設置一個標誌;
(2)讓各線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起;
而輪詢標誌的地方和Safepoint是重合的;
在JIT執行方式下:test指令是HotSpot生成的輪詢指令;一條test彙編指令便完成Safepoint輪詢和觸發線程中斷;
這就須要安全區域來解決;程序不執行時沒有CPU時間(Sleep或Blocked狀態),沒法運行到Safepoint上再中斷掛起;
(1)線程執行進入Safe Region,首先標識本身已經進入Safe Region;
(2)線程被喚醒離開Safe Region時,其須要檢查系統是否已經完成根節點枚舉(或整個GC);
若是已經完成,就繼續執行;不然必須等待,直到收到能夠安全離開Safe Region的信號通知,這樣就不會影響標記結果;雖然HotSpot虛擬機中採用了這些方法來解決對象可達性分析的問題,但只是大大減小了這些問題影響,並不能徹底解決,如GC停頓"Stop The World"是垃圾回收重點關注的問題,後面介紹垃圾回收器時應注意:低GC停頓是其一個關注。
下面先來了解Java虛擬機垃圾回收的幾種常見算法:標記-清除算法、複製算法、標記-整理算法、分代收集算法、火車算法,介紹它們的算法思路,有什麼優勢和缺點,以及主要應用場景。
(A)標記
首先標記出全部須要回收的對象;標記過程以下
(1)第一次標記在可達性分析後發現對象到GC Roots沒有任何引用鏈相連時,被第一次標記;
而且進行一次篩選:此對象是否必要執行finalize()方法;
對有必要執行finalize()方法的對象,被放入F-Queue隊列中;
(2)第二次標記
GC將對F-Queue隊列中的對象進行第二次小規模標記;
在其finalize()方法中從新與引用鏈上任何一個對象創建關聯,第二次標記時會將其移出"即將回收"的集合;
對第一次被標記,且第二次還被標記(若是須要,但沒有移出"即將回收"的集合),就能夠認爲對象已死,能夠進行回收。
(B)清除
兩次標記後,還在"即將回收"集合的對象將被統一回收;
執行過程以下圖:
二、優勢
(A)效率問題
標記和清除兩個過程的效率都不高;
(B)空間問題
標記清除後會產生大量不連續的內存碎片;這會致使分配大內存對象時,沒法找到足夠的連續內存;從而須要提早觸發另外一次垃圾收集動做;
四、應用場景針對老年代的CMS收集器;
執行過程以下圖:
二、優勢
三、缺點
(A)空間浪費
可用內存縮減爲原來的一半,太過浪費(解決:能夠改良,不按1:1比例劃分);
(B)效率隨對象存活率升高而變低
當對象存活率較高時,須要進行較多複製操做,效率將會變低(解決:後面的標記-整理算法);
四、應用場景(A)弱代理論
分代垃圾收集基於弱代理論(weak generational hypothesis),具體描述以下:
(1)大多數分配了內存的對象並不會存活太長時間,在處於年輕代時就會死掉;
(2)不多有對象會從老年代變成年輕代;
其中IBM研究代表:新生代中98%的對象都是"朝生夕死";
因此並不須要按1:1比例來劃份內存(解決了缺點1);
(B)HotSpot虛擬機新生代內存佈局及算法
(1)將新生代內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間;默認Eden:Survivor=8:1,即每次可使用90%的空間,只有一塊Survivor的空間被浪費;
(C)分配擔保(1)標記
標記過程與"標記-清除"算法同樣;
(2)整理
但後續不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動;
而後直接清理掉端邊界之外的內存;
執行過程以下圖:
2 優勢
(A)不會像複製算法,效率隨對象存活率升高而變低
老年代特色:
對象存活率高,沒有額外的空間能夠分配擔保;
因此老年代通常不能直接選用複製算法算法;
而選用標記-整理算法;
(B)不會像標記-清除算法,產生內存碎片
由於清除前,進行了整理,存活對象都集中到空間一側;
3 缺點如Serial Old收集器、G1(從總體看);
(A)新生代
每次垃圾收集都有大批對象死去,只有少許存活;因此可採用複製算法;
(B)老年代
對象存活率高,沒有額外的空間能夠分配擔保;使用"標記-清理"或"標記-整理"算法;
結合上面對新生代的內存劃分介紹和上篇文章對Java堆的介紹,能夠得出HotSpot虛擬機通常的年代內存劃分,以下圖:
二、優勢能夠根據各個年代的特色採用最適當的收集算法;
三、缺點仍然不能控制每次垃圾收集的時間;
四、應用場景如HotSpot虛擬機中所有垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留);
(1)選擇標號最小的火車;
(2)若是火車的記憶集合是空的, 釋放整列火車並終止, 不然進行第三步操做;
(3)選擇火車中標號最小的車箱;
(4)對於車箱記憶集合的每一個元素:
若是它是一個被根引用引用的對象, 那麼, 將拷貝到一列新的火車中去;
若是是一個被其它火車的對象指向的對象, 那麼, 將它拷貝到這個指向它的火車中去.;
假設有一些對象已經被保留下來了, 那麼經過這些對象能夠觸及到的對象將會被拷貝到同一列火車中去;
若是一個對象被來自多個火車的對象引用, 那麼它能夠被拷貝到任意一個火車去;
這個步驟中, 有必要對受影響的引用集合進行相應地更新;
(5)、釋放車箱而且終止;
收集過程會刪除一些空車廂和空車,當須要的時候也會建立一些車廂和火車。
執行過程以下圖:
二、優勢
垃圾收集器是垃圾回收算法(標記-清除算法、複製算法、標記-整理算法、火車算法)的具體實現,不一樣商家、不一樣版本的JVM所提供的垃圾收集器可能會有很在差異,下面主要介紹HotSpot虛擬機中的垃圾收集器。
JDK7/8後,HotSpot虛擬機全部收集器及組合(連線),以下圖:
新生代收集器 :Serial、ParNew、Parallel Scavenge;Serial/Serial Old組合收集器運行示意圖以下:
ParNew/Serial Old組合收集器運行示意圖以下:
(A)有一些特色與ParNew收集器類似
新生代收集器;
採用複製算法;
多線程收集;
(B)主要特色是:它的關注點與其餘收集器不一樣
CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間;
而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput);
關於吞吐量與收集器關注點說明詳見本節後面;
上面介紹的都是新生代收集器,接下來開始介紹老年代收集器;
Serial Old是 Serial收集器的老年代版本;
一、特色
針對老年代;
採用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);
單線程收集;
Serial/Serial Old收集器運行示意圖以下:
Parallel Scavenge/Parallel Old收集器運行示意圖以下:
整個過程當中耗時最長的併發標記和併發清除均可以與用戶線程一塊兒工做;
CMS收集器運行示意圖以下:
CMS收集器3個明顯的缺點
(A)對CPU資源很是敏感
(B)沒法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗
(C)產生大量內存碎片
一、特色
(A)並行與併發
(C)結合多種垃圾收集算法,空間整合,不產生碎片
從總體看,是基於標記-整理算法;
從局部(兩個Region間)看,是基於複製算法;
這是一種相似火車算法的實現;都不會產生內存碎片,有利於長時間運行;
(D)可預測的停頓:低停頓的同時實現高吞吐量
G1除了追求低停頓處,還能創建可預測的停頓時間模型;
能夠明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒;
不計算維護Remembered Set的操做,能夠分爲4個步驟(與CMS較爲類似)。
(A)初始標記(Initial Marking)
僅標記一下GC Roots能直接關聯到的對象;
且修改TAMS(Next Top at Mark Start),讓下一階段併發運行時,用戶程序能在正確可用的Region中建立新對象;須要"Stop The World",但速度很快;
(B)併發標記(Concurrent Marking)
進行GC Roots Tracing的過程;剛纔產生的集合中標記出存活對象;耗時較長,但應用程序也在運行;並不能保證能夠標記出全部的存活對象;
(C)最終標記(Final Marking)
爲了修正併發標記期間因用戶程序繼續運做而致使標記變更的那一部分對象的標記記錄;
上一階段對象的變化記錄在線程的Remembered Set Log;
這裏把Remembered Set Log合併到Remembered Set中;
須要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
採用多線程並行執行來提高效率;
(D)篩選回收(Live Data Counting and Evacuation)
首先排序各個Region的回收價值和成本;
而後根據用戶指望的GC停頓時間來制定回收計劃;
最後按計劃回收一些價值高的Region中垃圾對象;
回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另外一個空的Region,而且在此過程當中壓縮和釋放內存;
能夠併發進行,下降停頓時間,並增長吞吐量;
少數狀況下,可能直接分配在老年代中。
分配的細節取決於當前使用哪一種垃圾收集器組合,以及JVM中內存相關參數設置。
接下來將會講解幾條最廣泛的內存分配規則。
默認Eden:Survivor=8:1,即每次可使用90%的空間,只有一塊Survivor的空間被浪費;
大多數狀況下,對象在新生代Eden區中分配;
當Eden區沒有足夠空間進行分配時,JVM將發起一次Minor GC(新生代GC);
Minor GC時,若是發現存活的對象沒法所有放入Survivor空間,只好經過分配擔保機制提早轉移到老年代。
常常出現大對象容易致使內存還有很多空間就提早觸發GC,以獲取足夠的連續空間來存放它們,因此應該儘可能避免使用建立大對象;
JVM給每一個對象定義一個對象年齡計數器,其計算流程以下:
在Eden中分配的對象,經Minor GC後還存活,就複製移動到Survivor區,年齡爲1;
然後每經一次Minor GC後還存活,在Survivor區複製移動一次,年齡就增長1歲;
若是年齡達到必定程度,就晉升到老年代中;
若是在Survivor空間中相同年齡的全部對象大小總和大於Survivor空間的一半,大於或等於該年齡的對象就能夠直接進入老年代
(1)該類全部實例都已經被回收(即Java椎中不存在該類的任何實例);
(2)加載該類的ClassLoader已經被回收,也即經過引導程序加載器加載的類不能被回收;
(3)該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法;
一、CGLib在Spring、Hibernate等框架中對類進行加強時會使用;
二、VM的動態語言也會動態建立類來實現語言的動態性;
三、另外,JSP(第一次使用編譯爲Java類)、基於OSGi頻繁自定義ClassLoader的應用(同一個類文件,不一樣加載器加載視爲不一樣類)等;
從OS請求空間,而後分紅塊;
類加載器從它的塊中分配元數據的空間(一個塊被綁定到一個特定的類加載器);
當爲類加載器卸載類時,它的塊被回收再使用或返回到操做系統;
元數據使用由mmap分配的空間,而不是由malloc分配的空間;
三、相關參數下面介紹的是一些思路,並不是是具體的參數設置。
(1)停頓時間
GC停頓時間越短就適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗;
與用戶交互較多的場景,以給用戶帶來較好的體驗;
如常見WEB、B/S系統的服務器上的應用;
(2)吞吐量
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);
高吞吐量能夠高效率地利用CPU時間,儘快完成運算的任務,主要適合在後臺計算而不須要太多交互的任務;
應用程序運行在具備多個CPU上,對暫停時間沒有特別高的要求;
程序主要在後臺進行計算,而不須要與用戶進行太多交互;
例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;
(3)覆蓋區(Footprint)
在達到前面兩個目標的狀況下,儘可能減小堆的內存空間,以得到更好的空間局部性;
能夠減小到不知足前兩個目標爲止,而後再解決未知足的目標;
若是是動態收縮的堆設置,堆的大小將隨着垃圾收集器試圖知足競爭目標而振盪;
總結就是:低停頓、高吞吐量、少用內存資源;
通常這些目標都相互影響的,增大堆內存得到高吞吐量但會增加停頓時間,反之亦然,有時需折中處理。
通常都會先根據平臺性能來選擇好垃圾收集器,以及設置好其參數;
在運行中,一些收集器還會收集監控信息來自動地、動態的調整垃圾回收策略;
因此當咱們不知道何如選擇收集器和調整時,應該首先讓JVM自適應調整;
若是不能知足,或者經過打印設置的參數信息,發現能夠有更好的調優時,能夠進行手動指定參數進行設置,並測試;
沒有最好的收集器,更沒有萬能的收集;
選擇的只能是對具體應用最適合的收集器;
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
到實踐調優階段,那必需要了解每一個具體收集器的行爲特色、優點和劣勢、調節參數等(請參考前面的文章內容);而後根據明確指望的目標,選擇具體應用最適合的收集器;
當選擇使用某種並行垃圾收集器時,應該指按期望的具體目標而不是指定堆的大小;
讓垃圾收集器自動地、動態的調整堆的大小來知足指望的行爲;
即堆的大小將隨着垃圾收集器試圖知足競爭目標而振盪;
固然有時發現問題,堆的大小、劃分也是須要進行一些調整的,通常規則:除非應用程序沒法接受長時間的暫停,不然能夠將堆調的儘量大一些;
除非發現問題的緣由在於老年代的垃圾收集或應用程序暫停次數過多,不然你應該將堆的較大部分分給年輕代;
等等…
例如,使用Parallel Scavenge/Parallel Old組合,這是一種值得推薦的方式:
一、只需設置好內存數據大小(如"-Xmx"設置最大堆);
二、而後使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給JVM設置一個優化目標;
三、那些具體細節參數的調節就由JVM自適應完成;
設置調整後,應該經過在產生環境下進行不斷測試,來分析是否達到咱們的目標;
引用: