垃圾收集器與內(nèi)存分配策略
@(Java虛擬機)[垃圾收集, GC]
[TOC]
對象已死嗎
程序計數(shù)器贸呢,虛擬機棧冯键,本地方法棧隨線程回收而回收窄坦,而Java堆和方法區(qū)不會回收拴驮,對象也是動態(tài)創(chuàng)建春瞬。這部分區(qū)域是垃圾回收的主要區(qū)域。
引用計數(shù)算法
給對象添加一個引用計數(shù)器套啤,每當(dāng)引用時宽气,計數(shù)器加1,引用失效計數(shù)器減1潜沦。為0的對象就是不在使用的萄涯。引用計數(shù)簡單有效率,但是主要無法解決對象之間的相互循環(huán)引用問題止潮。
eg:A.a=B.b; B.b=A.a
可達(dá)性分析算法
可達(dá)性分析以'GC Roots'對象作為起點窃判,向下搜索钞楼,走過路徑稱為引用鏈喇闸。一個對象到GC Roots沒有任何引用鏈即從GC Roots到對象不可達(dá),則該對象不可用询件。
在Java語言中可以作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象燃乍。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象宛琅。
- 本地方法棧中JNI(Native方法)引用的對象刻蟹。
再談引用
判斷對象是否存活與“引用”有關(guān)。在JDK1.2前嘿辟,對引用定義很簡單舆瘪,如果reference類型的數(shù)據(jù)存儲的數(shù)值代表另外一塊內(nèi)存的起始地址片效,這塊內(nèi)存代表著一個引用。在JDK1.2以后Java對引用概念進行擴充英古,將引用分為4種:(強度依次減弱)
- 強引用(Strong Reference) eg:Object obj=new Object()淀衣;強引用還存在,就不回收被引用的對象召调。
- 軟引用(Soft Reference)有用但非必須對象膨桥,系統(tǒng)在發(fā)生內(nèi)存溢出異常前,會將這些對象列進回收范圍中唠叛。
- 弱引用(Weak Reference)非必須對象只嚣,下次垃圾收集回收。
- 虛引用(Phantom Reference)對象是否有虛引用不會對其生存時間構(gòu)成影響艺沼,也無法通過虛引用取得對象實例册舞,設(shè)置虛引用的作用在于這個對象被垃圾收集器回收時收到一個系統(tǒng)通知。
生存還是死亡
在可達(dá)性分析算法中不可達(dá)的對象障般,也并非“非死不可”环础,對象暫時處于“緩刑階段”,真正死亡需要至少兩次的標(biāo)記過程剩拢。
對象不可達(dá)會被第一次標(biāo)記并且篩選线得,篩選條件是對象是否需要執(zhí)行finalize方法。(對象沒有覆蓋finalize方法徐伐,或者finalize已經(jīng)被虛擬機觸發(fā)過贯钩,虛擬機則認(rèn)為是沒有必要執(zhí)行)
如果需要執(zhí)行finalize方法的,對象會被放入F-Queue隊列中办素,并且會被虛擬機創(chuàng)建的Finalizer(低優(yōu)先級)線程去調(diào)用finalize方法角雷。稍后GC會對F-Queue中的對象進行第二次小規(guī)模標(biāo)記,如果在finalize過程中該對象又和可達(dá)對象建立了關(guān)聯(lián)性穿,則在第二次標(biāo)記時會被移除“即將回收”集合勺三。
注:
1.finalize方法只會被系統(tǒng)調(diào)用一次。如果對象被調(diào)用過此方法需曾,而且面臨下次回收吗坚,它的finalize不會被執(zhí)行。
2.finalize不確定性大呆万,實用性不大商源。回收工作不如try-finally等方式谋减。建議忘記此方法的存在牡彻。
回收方法區(qū)
方法區(qū)(或者HotSpot的永久代)垃圾回收主要是兩部分內(nèi)容:廢棄常量和無用的類。
廢棄常量判定:字符串“abc”在常量池中出爹,但系統(tǒng)沒有一個String對象指向它庄吼。常量池中其他類(接口)缎除,方法,字段的符號引 用與此類似总寻。
無用類判定:3個條件
- 該類實例已全部回收伴找。Java堆中不存在該類實例了。
- 加載該類的ClassLoader已經(jīng)被回收废菱。
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用技矮。無法在任何地方通過反射訪問該類的方法
滿足上述條件后虛擬機可以對無用類回收了。但不一定必然會回收殊轴。是否對類回收HotSpot提供-Xnnclassgc參數(shù)控制衰倦。
垃圾收集算法
標(biāo)記-清除算法
先標(biāo)記,在清除旁理。不足之處:1.效率問題樊零,標(biāo)記和清除效率都不高。2.空間問題孽文,清除后產(chǎn)生不連續(xù)內(nèi)存碎片驻襟。
復(fù)制算法
將內(nèi)存分為兩塊,每次使用一塊芋哭,一塊內(nèi)存用完時沉衣,就將還存活的對象復(fù)制到另外一塊上。前面內(nèi)存空間一次清理掉减牺。
不足:浪費內(nèi)存空間豌习。存活對象多時,復(fù)制效率不高拔疚。
優(yōu)化方案:
大部分對象死得快肥隆。劃分比例改一改:HotSpot--> 8(Eden)+1(Survivor)+1(Survivor)。浪費最后一份Survivor即可稚失。
缺點:極端情況栋艳,1份裝不下存活的對象,就需要其他內(nèi)存空間做擔(dān)保句各。
標(biāo)記-整理算法
和標(biāo)記-清除算法類似吸占,但是后續(xù)操作不是清理而是讓存活對象移動到一端。清理端邊界以外的內(nèi)存
分代收集算法
當(dāng)前商業(yè)虛擬機都采用此算法诫钓。根據(jù)對象的存活周期不同將內(nèi)存劃分為幾塊旬昭。將Java堆分為新生代和老年代篙螟,在新生代中使用復(fù)制算法菌湃,在老年代中使用標(biāo)記-清理或者標(biāo)記-整理算法。
HotSpot的算法實現(xiàn)
枚舉根節(jié)點
使用OopMap來記錄那些位置是引用遍略,不需要對整個全局性引用和棧幀的本地變量表遍歷惧所。節(jié)約時間骤坐。
GC時需要暫所有的Java執(zhí)行線程使分析可靠。
安全點
在OopMap的協(xié)助下下愈,HotSpot快速完成GC Roots枚舉纽绍。為了節(jié)約空間不是所有的指令會產(chǎn)生OopMap。安全點位置才會產(chǎn)生势似,也是在安全點下才能GC拌夏。
安全點選取條件:是否具有讓程序長時間執(zhí)行的特征。
長時間執(zhí)行的特征是指令序列復(fù)用履因,例如方法調(diào)用障簿,循環(huán)跳轉(zhuǎn),異常跳轉(zhuǎn)等栅迄。這些地方的指令才會產(chǎn)生SafePoint站故。
GC發(fā)生時讓所有線程都跑到安全點停下來:
搶先式中斷:GC發(fā)生所有線程中斷,如果線程中斷不在安全點上毅舆,則恢復(fù)線程跑到安全點上西篓。基本沒有虛擬機這樣做
主動式中斷:設(shè)置標(biāo)志憋活,GC發(fā)生時岂津,線程輪詢這個標(biāo)志,中斷標(biāo)志為真則自己中斷悦即,輪詢標(biāo)志的地方和安全點重合寸爆。
安全區(qū)域
線程不執(zhí)行時(處于Sleep和Blocked下),無法響應(yīng)JVM的中斷請求盐欺。這種情況需要安全區(qū)域配合赁豆。
安全區(qū)域:指這段代碼片段中不會引起引用關(guān)系的變化。在這區(qū)域中GC都是安全的冗美。
線程執(zhí)行到Safe Region中時魔种,首先標(biāo)記自己是進入Safe Region狀態(tài)。當(dāng)要離開Safe Region時需要檢查是否完成了GC粉洼,沒有完成就必須等待可以離開信號為止节预。
垃圾收集器
圖中展示7中收集器在不同分代中工作。連線代表搭配使用属韧。
Serial收集器
Serial收集器不僅僅是使用一個CPU或者一條線程去完成垃圾收集安拟,更重要的是它收集垃圾時必須暫停其他工作線程。
Serial收集器在Client模式下是很好的選擇
新生代采用復(fù)制算法宵喂,暫停所有用戶線程糠赦,老年代采用標(biāo)記-整理算法,暫停所有用戶線程
ParNew收集器
ParNew收集器是Serial的多線程版本。和Serial收集器沒有太多創(chuàng)新之處拙泽。但它是Server模式下的虛擬機的首選新生代收集器淌山。ParNew是除了serial外能和CMS(Concurrent Mark Sweep)搭配使用的收集器。ParNew在單CPU下效果不會有Serial的好顾瞻。
Parallel Scavenge收集器
Parallel Scavenge是新生代收集器泼疑,使用復(fù)制算法,并行的多線程收集器荷荤。
Parallel Scaveng收集器關(guān)注點和其他收集器不一樣退渗,CMS等收集器的關(guān)注點是盡可能縮短垃圾收集時用戶線程的停頓時間。而此收集器關(guān)注吞吐量蕴纳。
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)氓辣。
Parallel Scavenge可以使用GC自適應(yīng)的調(diào)節(jié)策略來自動動態(tài)調(diào)整參數(shù)。
Serial Old收集器
Serial Old是Serial的老年代版本袱蚓,也是單線程收集器钞啸,使用標(biāo)記-整理算法。主要給Client模式的虛擬機使用喇潘。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本体斩,使用多線程和“標(biāo)記-整理”算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器以獲取最短回收停頓時間為目標(biāo)颖低。主要應(yīng)用于B/S系統(tǒng)上絮吵。CMS采用標(biāo)記-清除算法實現(xiàn)。分4個步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清除
初始標(biāo)記和重新標(biāo)記需要Stop the world忱屑,初始標(biāo)記GC Roots能直接關(guān)聯(lián)的對象蹬敲,速度快。并發(fā)標(biāo)記進行GC Roots Tracing過程莺戒。重新標(biāo)記對在并發(fā)標(biāo)記過程中有部分對象標(biāo)記產(chǎn)生變動的一部分重新標(biāo)記伴嗡。時間比初始標(biāo)記長,但比并發(fā)標(biāo)記短得多从铲。
CMS過程中耗時較長的部分是并發(fā)的瘪校,所以整體上能和用戶線程一起并發(fā)執(zhí)行。
CMS缺點:
- 對CPU資源非常敏感(并發(fā)導(dǎo)致)名段,CMS默認(rèn)啟動回收線程數(shù)(CPU數(shù)量+3)/4阱扬,CPU數(shù)量少于4個時,影響很大伸辟。
- 無法處理浮動垃圾麻惶,可能導(dǎo)致Concurrent Mode Failure 失敗導(dǎo)致另一次Full GC。
- 標(biāo)記-清除算法產(chǎn)生大量碎片信夫,內(nèi)存空間連續(xù)的無法分配大對象時需要Full GC窃蹋。
G1收集器
G1是面向服務(wù)端應(yīng)用的收集器卡啰,G1具備的特點:
- 并行與并發(fā):充分利用多CPU,多核硬件優(yōu)勢脐彩。
- 分代收集:可以獨立管理整個GC堆碎乃,不需其他收集器配合姊扔。
- 空間整合:G1整體看來是基于標(biāo)記——整理算法實現(xiàn)惠奸。從局部看是基于復(fù)制算法
- 可預(yù)測停頓:可指定在M毫秒中,垃圾收集不超過N毫秒
G1將內(nèi)存分為多個大小相等的獨立區(qū)域Region恰梢,還保留新生代和老年代的概念佛南,但不再是物理隔離的,都是是一部分Region(不需連續(xù))的集合嵌言。G1避免在全區(qū)域垃圾回收嗅回,可以對單個Region回收,后臺維護一個優(yōu)先列表摧茴,來決定回收那些Region绵载。
Remembered Set步驟:保證不對全堆掃描也不會遺漏。記錄在不同Region中的對象引用苛白。
G1收集器步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 最終標(biāo)記
- 篩選回收
理解GC日志
33.3125和100.667代表GC發(fā)生時間娃豹,Java虛擬機啟動以來的秒數(shù)。
Full GC代表stop the world
DefNew购裙,Tenured懂版,Perm代表GC發(fā)生區(qū)域。顯示的區(qū)域名和GC收集器相關(guān)躏率,DefNew代表Serial新生代躯畴,ParNew代表Parallel新生代,PSYoungGen代表Parallel Scavenge新生代薇芝。
3324K-》152K(3721K):GC前該區(qū)域內(nèi)存區(qū)域已使用容量-》GC后改內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘浚?br>
3324K-》152K(11904K):GC前Java堆已使用容量-》GC后Java堆已使用容量(Java堆總?cè)萘浚?br>
0.0025925secs表示該內(nèi)存區(qū)域GC所占用的時間蓬抄。單位秒
垃圾收集相關(guān)常用參數(shù)
內(nèi)存分配與回收策略
對象主要分配在新生代Eden區(qū)上,少數(shù)情況會分配在老年代中夯到。規(guī)則不固定倡鲸,細(xì)節(jié)由垃圾收集器和參數(shù)決定。
下面使用Serial/Serial Old收集器的內(nèi)存分配和回收策略黄娘。
對象優(yōu)先在Eden分配
private static final int _1MB=1024*1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M 限制Java堆大小為20M峭状,不可擴展,10M新生代逼争,10M老年代
* -XX:+PrintGCDetails
* -XX:SurvivorRatio=8決定新生代中Eden區(qū)與Survivor區(qū)=8:1
*/
public static void testAllocation(){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1=new byte[2*_1MB];
allocation2=new byte[2*_1MB];
allocation3=new byte[2*_1MB];
allocation4=new byte[4*_1MB]; //出現(xiàn)一次Minor GC
}
eden space 8192K优床,from space 1024K,to space 1024K誓焦,新生代9216K胆敞。當(dāng)分配allocation4時着帽,內(nèi)存不足,發(fā)生Minor GC移层,1,2,3被轉(zhuǎn)到老年區(qū)仍翰,然后4被分配在Eden中。Survivor空閑观话。
新生代GC(Minor GC):發(fā)生頻繁予借,速度快
老年代GC(Major/Full GC):速度慢,一般伴隨Minor GC
大對象直接進入老年代
虛擬機提供-XX:pretenureSizeThreshold參數(shù)频蛔,大于這個值的對象直接分配到老年代灵迫,避免在Eden和兩個Survivor發(fā)生復(fù)制。
注:pretenureSizeThreshold只對Serial和ParNew有效晦溪。
長期存活對象將進入老年代
給對象定義了對象年齡計數(shù)器瀑粥,對象在Eden出生并在minor GC后任存活,并被Survivor容納三圆,將移動到Survivor中狞换,對象年齡設(shè)為1,每過一次Minor GC舟肉,年齡加1修噪。默認(rèn)到15,就會轉(zhuǎn)入老年代。參數(shù)值可用-XX:MaxTenuringThreshold設(shè)置度气。
動態(tài)對象年齡判定
如果Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半割按,年齡大于該年齡的對象進入老年代。就不需MaxTenuringThreshold中要求的年齡磷籍。
空間分配擔(dān)保
在Minor GC前适荣,虛擬機先檢查老年代連續(xù)可用空間是否大于新生代對象總空間,如果成立院领,則Minor GC安全弛矛。不成立
虛擬機先查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。允許則檢查老年代連續(xù)空間是否大于歷次轉(zhuǎn)入老年代對象的平均值比然。大于則Minor GC(有風(fēng)險)丈氓。小于或者HandlePromotionFailure不允許冒險,則Full GC强法。JDK6 Update14后HandlePromotionFailure參數(shù)不在使用万俗。
風(fēng)險是因為老年代需要做擔(dān)保。