垃圾收集器與記憶體分配策略 由於JVM中對象的頻繁操作是在堆中,所以主要回收的是堆記憶體,方法區中的回收也有,但是比較謹慎 一、對象死亡判斷方法 1.引用計數法 就是如果對象被引用一次,就給計數器+1,否則-1 實現簡單,但是無法解決對象相互引用的問題;實際上JVM也不是使用的此種方式,因此已下的程式我 ...
垃圾收集器與記憶體分配策略
由於JVM中對象的頻繁操作是在堆中,所以主要回收的是堆記憶體,方法區中的回收也有,但是比較謹慎
一、對象死亡判斷方法
1.引用計數法
就是如果對象被引用一次,就給計數器+1,否則-1
實現簡單,但是無法解決對象相互引用的問題;實際上JVM也不是使用的此種方式,因此已下的程式我們會看到記憶體被回收了
/** *testGC()方法執行後,objA和objB會不會被GC呢? *@author zzm */ class ReferenceCountingGC{ public Object instance=null; private static final int _1MB = 1024*1024; /** *這個成員屬性的唯一意義就是占點記憶體,以便能在GC日誌中看清楚是否被回收過 */ private byte[]bigSize=new byte[2*_1MB]; public static void testGC(){ ReferenceCountingGC objA=new ReferenceCountingGC(); ReferenceCountingGC objB=new ReferenceCountingGC(); objA.instance=objB; objB.instance=objA; objA=null; objB=null; //假設在這行發生GC,objA和objB是否能被回收? System.gc(); } }
2.可達性分析
定義一些GCroot,如果從GCroot到對象是不可達的,那麼對象就可以被回收
可能的gcroot:棧中的存放的對象的引用、方法區中靜態屬性和常量引用的對象、本地方法棧引用的對象(native)
主流的jvm都是使用的此種方式
3.引用
無論通過什麼方式,都是通過“引用”來判斷!
在JVM中引用分為四種:強、軟、弱、虛(具體參考這篇文章:http://www.cnblogs.com/zhangxinly/p/6978355.html)
4.finalize方法
如果對象不可達,對象將被標記,
類覆寫了此方法,且對象的此方法從未被JVM執行過,則對象被放入一個隊列,等待一個線程來執行此對象的方法(註意只會執行一次)
可以使用這種特性在對象不可達,被髮現為可回收的狀態下,重新回收對象;就是在finalize方法中重新建立強引用
不建議使用,瞭解即可
/** * 此代碼演示了兩點: * 1.對象可以在被GC時自我拯救。 * 2.這種自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統自動調用一次 * * @author zzm */ class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes,i am still alive:)"); } protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize mehtod executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //對象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); //因為finalize方法優先順序很低,所以暫停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no,i am dead:("); } //下麵這段代碼與上面的完全相同,但是這次自救卻失敗了 SAVE_HOOK = null; System.gc(); //因為finalize方法優先順序很低,所以暫停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no,i am dead:("); } } }
5.方法區回收
方法區中的常量回收:在程式中沒有任何地方使用到如:str=“abc”,則回收
類回收:類所有實例全被回收、類載入器被回收、類的Class對象未被引用無法在任何地方通過反射調用,則該類可以被回收卸載
在如今框架動態代理大行其道的今天,JVM必須有卸載類的方法,不然出現泄漏
二、回收演算法
1.標記-清除演算法
標記不可達對象,然後jvm進行統一回收
缺點:
效率不高,兩個過程效率都不高
回收後記憶體不連續,因為是從中移除掉不可達的,會導致大量碎片,如果JVM要分配一個連續的大記憶體,將會產生問題
2.複製演算法
1)將記憶體分為兩份A和B,如果A不夠用了,就將A中存活的對象複製到B中(複製過去的肯定小於等於原來的)
2)然後將A清空,等待B滿了之後再次執行相反的動作;迴圈往複
問題:記憶體只能使用一半
優點:迅速,複製之後記憶體空間連續
使用:在新生代中,對象的創建和死亡是十分快的,這就保證了每次從A複製到B中的會很少(大量的被回收),所以A和B不需要一樣大,甚至B可以很小
在主流虛擬機中使用的是這種方法,分為A/B/C三快,比例為8:1:1,將A和B複製到C
3.標記整理
也是將需要回收的標記
然後不統一回收,而是將存活的統一移動到一端
最後將端外的全部回收
4.分代演算法
分代:根據對象的存活周期將記憶體分代如:新生代(對象創建死亡活躍)和老年代(比較穩定)
根據以上的介紹:在新生代中就適合用複製演算法,在老年代中就適合用標記整理/清理演算法
就是複製+標記整理兩種演算法結合
三、HotSpot的演算法實現
1.枚舉根節點
根節點很多,如果要逐個檢查這裡面的引用會浪費時間
GC停頓,為了在gc時引用狀態不改變,需要停頓所有執行線程,直至gc完成
所以:JVM有方法直接知道哪些地方存放這引用;通過oopMap這樣的數據結構來實現
oop:
在類載入完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。
這樣,GC在掃描時就可以直接得知這些信息了
2.安全點
在特定的位置記錄信息,進行GC;此時需要讓線程都跑到安全點掛起
這裡有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)
其中搶先式中斷不需要線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢複線程,讓它“跑”到安全點上。
而主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌
發現中斷標誌為真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的
3.安全區域
如果程式沒有執行:沒有CUP時間片(sleep、blocked等),線程是無法響應中斷的,也就沒法去安全點進行掛起
解決:使用安全區域,在safe region中任意地方開始GC都是安全的,就不需要線程跑到安全點了
流程:
當線程進入safe region時標識自己進入safe region;
當GC時不管safe region狀態的線程;
當線程要出來時,要判斷系統是否已經枚舉GCroot完成,否則要等待其完成才能出safe region
四、垃圾收集器
先來一張圖,瞭解HotSpot中的垃圾收集器;
1.Serial收集器(單線程,新生代)
特點:
這是一個單線程收集器
收集時,停止所有工作線程,直到它工作結束
會導致程式停頓
場景:
在單cpu環境中,簡單高效,沒有線程交互開銷,專心做垃圾收集
在桌面客戶端client應用中,交給JVM的記憶體管理不會太多,使用也不會造成長時間停頓
2.ParNew收集器(多線程,老年代)
可以認為是Serial的多線程版本;
特點:
多線程並行收集;但是用戶線程還是全部暫停
在多cup中有優勢,在單核系統中不一定比serial好,因為存線上程切換開銷
場景:
由於HotSpot推出了劃時代的CMS收集器作為老年代的收集器,卻只有ParNew能與之共同工作來收集新生代
3.Paraller Scavenge 收集器(吞吐量收集器,gc自適應調節)
新生代收集器,也是並行採用複製演算法,但是可以手動或自動調節cpu的吞吐量,
所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
GC停頓時間短適合需要與用戶交互的程式,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多交互的任務。
-XX:MaxGCPauseMillis參數:控制最大垃圾收集停頓時間的
MaxGCPauseMillis參數允許的值是一個大於0的毫秒數,收集器將儘可能地保證記憶體回收花費的時間不超過設定值。不過大家不要認為如果把這個參數的值設置得稍小一點就能使
得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導致垃圾
收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每
次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。
-XX:GCTimeRatio參數:直接設置吞吐量大小的
GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間占總時間的比率,相當於是吞吐量的倒數。
如果把此參數設置為19,那允許的最大GC時間就占總時間的5%(即1/(1+19)),預設值為99,就是允許最大1%(即1/(1+99))的垃圾收集時間。
+UseAdaptiveSizePolicy:這是一個開關參數
當這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數
虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)[1]。
只需要把基本的記憶體數據設置好(如-Xmx設置最大堆),然後使用MaxGCPauseMillis參數(更關註最大停頓時間)或GCTimeRatio(更關註吞吐量)參數給虛擬機設立一個優化目標
自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別
4.Serial Old
顧名思義,Serial的老年代版本
作為CMS收集器的後備預案,在併發收集Concurrent Mode Failure時使用
5.Parallel old收集器
顧名思義,Parallel的老年代版本
這樣就組成了:完整的新生代和老年代吞吐量收集器
6.CMS收集器(可併發)
階段:
初始標記(CMS initial mark):標記GC Roots能直接關聯到的對象,速度很快;
併發標記(CMS concurrent mark):gc可達性GC RootsTracing的過程
重新標記(CMS remark):修正併發標記期間因用戶程式繼續運作而導致標記產生變動的那一部分對象的標記記錄
併發清除(CMS concurrent sweep):清除標記的記憶體
詳解:
初始標記、重新標記這兩個步驟仍然需要“Stop The World”。
而重新標記階段這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比併發標記的時間短。
由於整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與用戶線程一起併發執行的。
缺點:
對CPU資源敏感:啟動的線程=(cpu+3)/4,也就是cpu多則占用整個系統的資源少,反之則相反;在cpu少的情況下會使系統突然變慢
浮動垃圾,由於在清除時,用戶線程還在運行產生垃圾,這些垃圾只能等下次GC
基於標記-清除,產生大量碎片;
7.G1收集器(最新)
可預測的停頓;標記整理+複製,無CMS的碎片問題
分析:
在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代;
G1收集器它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。
G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region
這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。
G1把記憶體“化整為零”的:
把Java堆分為多個Region後,垃圾收集是否就真的能以Region為單位進行了?
Region不可能是孤立的。一個對象分配在某個Region中,它並非只能被本Region中的其他對象引用,而是可以與整個Java堆任意的對象發生引用關係。
那在做可達性判定確定對象是否存活的時候,豈不是還得掃描整個Java堆才能保證準確性?
這個問題其實並非在G1中才有,只是在G1中更加突出而已。
在以前的分代收集中,新生代的規模一般都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象時也面臨相同的問題,如果回收新生代時也不得不同時掃描老年代的話,那麼Minor GC的效率可能下降不少。
在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。
G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程式在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處於不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。
當進行記憶體回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。
過程:與CMS很類似
初始標記(Initial Marking)
併發標記(Concurrent Marking)
最終標記(Final Marking)
篩選回收(Live Data Counting and Evacuation)
五、記憶體分配與回收策略
1.對象優先在Eden分配
2.大對象直接分配在老年代上:其閥值控制:-XX:PretenureSizeThreshould=位元組
3.長期存貨的對象進入老年代:其閥值(每經過一次複製,值+1):-XX:MaxTenuringThreshold=15
4.動態對象年齡判斷:
不一定一定要達到閥值才放入老年代:當Survivor中相同年齡的對象>=Survivor的一半時,這些對象直接進入老年代
5.空間分配擔保
當Eden存活對象複製入Survivor中,如果空間不夠,複製進入老年代中,在複製進老年代時,也要判斷空間大小(值為歷次進入老年代對象的平均值)
附錄:垃圾收集相關常數