一挂捅、判斷Java中對象存活的算法
1馅扣、引用計數(shù)器算法:
Java 堆
中每個具體對象(不是引用)都有一個引用計數(shù)器脐恩。當一個對象被創(chuàng)建并初始化賦值后舍肠,該變量計數(shù)設置為1
向族。每當有一個地方引用它時呵燕,計數(shù)器值就加1。當引用失效時件相,即一個對象的某個引用超過了生命周期(出作用域后)或者被設置為一個新值時再扭,計數(shù)器值就減1。任何引用計數(shù)為0
的對象可以被當作垃圾收集夜矗。當一個對象被垃圾收集時泛范,它引用的任何對象計數(shù)減1。
-
優(yōu)點:
引用計數(shù)收集器執(zhí)行簡單紊撕,判定效率高罢荡,交織在程序運行中。對程序不被長時間打斷的實時環(huán)境比較有利。 -
缺點:
難以檢測出對象之間的循環(huán)引用(A對象引用B對象区赵,B對象又引用A對象惭缰,但是A,B對象已不被任何其他對象引用)笼才。同時从媚,引用計數(shù)器增加了程序執(zhí)行的開銷。所以在JDK1.1之后患整,這個算法已經(jīng)不再使用了拜效。
2、可達性分析算法
可達性分析算法也叫根搜索算法各谚,通過一系列的稱為 GC Roots
的對象作為起點紧憾,然后向下搜索。搜索所走過的路徑稱為引用鏈 (Reference Chain
)昌渤, 當一個對象到 GC Roots
沒有任何引用鏈相連時, 即該對象不可達赴穗,也就說明此對象是 不可用的。
在Java
中, 可作為GC Roots
的對象包括以下四種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中靜態(tài)引用指向的對象膀息。也就是類中的 static 修飾的變量所引用的對象
- 方法區(qū)中常量引用的對象
-
本地方法棧中
JNI
(Native
方法)的引用的對象 - 仍處于存活狀態(tài)中的線程對象
如下圖所示: Object5般眉、Object6、Object7
雖然互有關聯(lián), 但它們到GC Roots
是不可達的, 因此也會被判定為可回收的對象潜支。
二甸赃、對象引用分類
1. 強引用(Strong Reference)
在代碼中普遍存在的,類似Object obj = new Object()
這類引用冗酿,只要強引用還在埠对,垃圾收集器永遠不會回收掉被引用的對象。
2. 軟引用(Sofe Reference)
有用但并非必需 的對象裁替,可用SoftReference
類來實現(xiàn)軟引用项玛。在系統(tǒng)將要發(fā)生內存溢出異常之前,將會把這些對象列進回收范圍之中進行二次回收弱判。如果這次回收還沒有足夠的內存襟沮,才會拋出內存溢出異常。
3. 弱引用(Weak Reference)
非必需的對象昌腰,但它的強度比軟引用更弱开伏,被弱引用關聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前,JDK提供了WeakReference
類來實現(xiàn)弱引用剥哑。無論當前內存是否足夠硅则,用軟引用相關聯(lián)的對象都會被回收掉淹父。
4. 虛引用(Phantom Reference)
虛引用也稱為幽靈引用或幻影引用株婴,是最弱的一種引用關系,JDK提供了PhantomReference
類來實現(xiàn)虛引用。為一個對象設置虛引用的唯一目的是:能在這個對象在垃圾回收器回收時收到一個系統(tǒng)通知困介。
三大审、JVM 垃圾回收算法
1、標記-清除算法
標記-清除(Mark-Sweep)算法是現(xiàn)代垃圾回收算法的思想基礎座哩。標記-清除算法將垃圾回收分為兩個階段:標記階段和清除階段徒扶。一種可行的實現(xiàn)是,在標記階段根穷,首先通過根節(jié)點姜骡,標記所有從根節(jié)點開始的可達對象。因此屿良,未被標記的對象就是未被引用的垃圾對象(好多資料說標記出要回收的對象圈澈,其實明白大概意思就可以了)。然后尘惧,在清除階段康栈,清除所有未被標記的對象。
優(yōu)點:
實現(xiàn)簡單喷橙,不需要進行對象進行移動啥么。缺點:
標記、清除過程效率低贰逾,產(chǎn)生大量不連續(xù)的內存碎片悬荣,提高了垃圾回收的頻率。
如圖:
2疙剑、標記整理算法
標記整理算法 采用和 標記-清除算法 一樣的方式進行對象的標記隅熙,但后續(xù)不直接對可回收對象進行清理,而是將所有的存活對象往一端空閑空間移動核芽,然后清理掉端邊界以外的內存空間囚戚。
優(yōu)點:
解決了標記-清理算法存在的內存碎片問題。
沒有內存碎片后轧简,對象創(chuàng)建內存分配也更快速了(可以使用TLAB進行分配)驰坊。缺點:
效率問題,(同標記清除算法)標記和整理兩個過程的效率都不高哮独;
仍需要進行局部對象移動拳芙,一定程度上降低了效率。
如圖:
3皮璧、復制算法
復制算法可以解決效率問題舟扎,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊悴务,當這一塊內存用完了睹限,就將還存活著的對象復制到另一塊上面譬猫,然后再把已經(jīng)使用過的內存空間一次清理掉,這樣使得每次都是對整個半?yún)^(qū)進行內存回收羡疗,內存分配時也就不用考慮內存碎片等復雜情況染服,只要移動堆頂指針,按順序分配內存即可(還可使用TLAB進行高效分配內存)叨恨。
-
優(yōu)點:
效率高柳刮,沒有內存碎片(按順序分配內存即可,實現(xiàn)簡單痒钝、運行高效秉颗,不用考慮內存碎片。) -
缺點:
可用的內存大小縮小為原來的一半送矩,對象存活率高時會頻繁進行復制站宗,效率將會變低。
如圖:
圖的上半部分是未回收前的內存區(qū)域益愈,圖的下半部分是回收后的內存區(qū)域梢灭。通過圖,我們發(fā)現(xiàn)不管回收前還是回收后都有一半的空間未被利用蒸其。
4敏释、分代收集算法
當前商業(yè)虛擬機都是采用分代收集算法,它根據(jù)對象存活周期的不同將內存劃分為幾塊摸袁,一般是把Java堆分為年輕代钥顽、老年代 和 永久代。然后根據(jù)各個年代的特點采用最適當?shù)氖占惴恐谛律蟹浯螅看卫占及l(fā)現(xiàn)有大批對象死去,只有少量存活蝶怔,就選用復制算法奶浦,而老年代因為對象存活率高,沒有額外空間對它進行分配擔保踢星,就必須使用“標記清理”或者“標記整理”算法來進行回收澳叉。
如圖:
圖的左半部分是未回收前的內存區(qū)域,右半部分是回收后的內存區(qū)域沐悦。
對象分配策略:
- 對象優(yōu)先在Eden區(qū)域分配成洗,如果對象過大直接分配到Old區(qū)域。
- 長時間存活的對象進入到Old區(qū)域藏否。
改進復制算法:
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代瓶殃,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”的副签,所以并不需要按照1:1的比例來劃分內存空間遥椿,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間基矮,每次使用Eden和其中一塊Survivor 。當回收時修壕,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上愈捅,最后清理掉Eden和剛才用過的Survivor空間遏考。
HotSpot 虛擬機
默認Eden和Survivor的大小比例是8:1慈鸠,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”灌具。當然青团,98%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活咖楣,當Survivor空間不夠用時督笆,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。
年輕代诱贿、老年代 和 永久代娃肿,如下圖所示:
新生代(Young generation)
絕大多數(shù)最新被創(chuàng)建的對象會被分配到這里,由于大部分對象在創(chuàng)建后會很快變得不可達珠十,所以很多對象被創(chuàng)建在新生代料扰,然后消失。對象從這個區(qū)域消失的過程我們稱之為 minor GC
焙蹭。
新生代 中存在一個Eden
區(qū)和兩個Survivor
區(qū)晒杈。新對象會首先分配在Eden
中(如果新對象過大,會直接分配在老年代中)孔厉。在GC
中拯钻,Eden
中的對象會被移動到Survivor
中,直至對象滿足一定的年紀(定義為熬過GC
的次數(shù))撰豺,會被移動到老年代粪般。
可以設置新生代和老年代的相對大小。這種方式的優(yōu)點是新生代大小會隨著整個堆大小動態(tài)擴展污桦。參數(shù) -XX:NewRatio
設置老年代與新生代的比例刊驴。例如 -XX:NewRatio=8
指定 老年代/新生代 為8/1
. 老年代占堆大小的 7/8
,新生代 占堆大小的 1/8
(默認即是 1/8
)寡润。
例如:
-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
老年代(Old generation)
對象沒有變得不可達捆憎,并且從新生代中存活下來,會被拷貝到這里梭纹。其所占用的空間要比新生代多躲惰。也正由于其相對較大的空間,發(fā)生在老年代上的GC
要比新生代要少得多变抽。對象從老年代中消失的過程础拨,可以稱之為major GC
(或者full GC
)氮块。
永久代(permanent generation)
像一些類的層級信息,方法數(shù)據(jù) 和方法信息(如字節(jié)碼诡宗,棧 和 變量大小)滔蝉,運行時常量池(JDK7
之后移出永久代),已確定的符號引用和虛方法表等等塔沃。它們幾乎都是靜態(tài)的并且很少被卸載和回收蝠引,在JDK8
之前的HotSpot
虛擬機中,類的這些永久的 數(shù)據(jù)存放在一個叫做永久代的區(qū)域蛀柴。
永久代一段連續(xù)的內存空間螃概,我們在JVM
啟動之前可以通過設置-XX:MaxPermSize
的值來控制永久代的大小。但是JDK8
之后取消了永久代鸽疾,這些元數(shù)據(jù)被移到了一個與堆不相連的稱為元空間 (Metaspace
) 的本地內存區(qū)域吊洼。
小結
JDK8
堆內存一般是劃分為年輕代和老年代,不同年代 根據(jù)自身特性采用不同的垃圾收集算法制肮。
對于新生代冒窍,每次GC
時都有大量的對象死亡,只有少量對象存活豺鼻∽垡海考慮到復制成本低,適合采用復制算法拘领。因此有了From Survivor
和To Survivor
區(qū)域意乓。
對于老年代,因為對象存活率高约素,沒有額外的內存空間對它進行擔保届良。因而適合采用標記-清理算法和標記-整理算法進行回收
參考: