1、對象已死男翰?
在對里面存放著Java世界中幾乎所有的對象實例纽乱,垃圾收集器在對堆進行回收前鸦列,第一件事情就是要確定這些對象之中那些還存活著,哪些已經(jīng)死去(即不可能再被任何途徑使用的對象)顽爹。
1.1 引用計數(shù)算法
給對象添加一個引用計數(shù)器镜粤,每當有一個地方引用它時玻褪,計數(shù)器值就加1;當引用失效時同规,計數(shù)器值就減1;任何時刻計數(shù)器為0的對象就是不可能再被使用绪钥。
優(yōu)點:實現(xiàn)簡單程腹,判定效率也很高
缺點:很難解決對象之間相互循環(huán)引用的問題
1.2 可達性分析算法
通過一系列的稱為“GC Roots”的對象作為起始點儒拂。從這些節(jié)點開始向下搜素侣灶。搜素所走過的路程稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時池户,則證明此對象是不可用的凡怎。如圖:
在Java語言中统倒,可作為GC Roots的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區(qū)中類靜態(tài)屬性引用的對象
方法區(qū)中常量引用的對象
本地棧中JNI(即一般說的Native方法)引用的對象
1.3 再談引用
傳統(tǒng)定義:如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址房匆,就稱這塊內(nèi)存代表著引用。
如今:引用分為強引用井氢、軟引用岳链、弱引用掸哑、虛引用、
強引用:只要強引用還存在厌蔽,垃圾收集器永遠不會回收掉被引用的對象俭嘁,類似 Object object=new Object()
軟引用:用來描述一些還有用但并非必須的對象供填。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前叉瘩,將會把這些對象列進回收范圍之中進行第二次回收
弱引用:用來描述非必須對象薇缅,但是其強度比軟引用更弱一些攒磨,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前娩缰。當垃圾收集器工作時,無論當前內(nèi)存是否足夠浮毯,都會回收掉只被弱引用關(guān)聯(lián)的對象
虛引用:也被稱為幽靈引用或者幻影引用泰鸡,是最弱的一種引用關(guān)系盛龄。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響蹦锋,也無法通過虛引用來取得一個對象實例欧芽。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被回收時收到一個系統(tǒng)通知千扔。
1.4 生存還是死亡
在可達性分析算法中不可達的對象,也并非是“非死不可”的厘唾,這時候它們暫時處于“緩刑”階段龙誊,要真正宣告一個對象死亡抚垃,至少要經(jīng)歷兩次標記過程:如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次刷選,刷選條件是此對象是否有必要執(zhí)行finalize()方法鹤树。當對象沒有覆蓋finalize方法铣焊,或者finalize方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”罕伯。
如果這個對象被判定為有必要執(zhí)行finalize()方法曲伊,那么這個對象將會放置在一個叫做F-Queue的隊列之中,并在稍后由一個由虛擬機自動建立的追他、低優(yōu)先級的Finalizer線程去執(zhí)行它。
這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法邑狸,但并不承諾會等待它運行結(jié)束懈糯,這樣做的原因是,如果一個對象在finalize()方法中執(zhí)行緩慢单雾,或者發(fā)生了死循環(huán)(更極端的情況)昂利,將很可能會導(dǎo)致F-Queue隊列中其他對象永久處于等待,甚至導(dǎo)致整個內(nèi)存回收系統(tǒng)崩潰铁坎。
任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次蜂奸,如果對象面臨下一次回收,它的finalize()方法不會被再次執(zhí)行
1.5 回收方法區(qū)
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類
回收廢棄常量與回收Java堆中的對象非常類似:假如一個字符串“abc”已經(jīng)進入了常量池中硬萍,但是當前系統(tǒng)沒有任何一個String對象是叫做“abc”的扩所,換句話說,就是沒有任何String對象引用常量池中的“abc”常量朴乖,也沒有比其他敵法引用了這個字面量祖屏,如果這時發(fā)生內(nèi)存回收,而且必要的話买羞,這個“abc”常量就會被系統(tǒng)清理出常量池袁勺。
判斷一個類是否“無用的類”的條件:
該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例
加載該類的ClassLoader已被回收
該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用畜普,五大在任何地方通過反射訪問該類的方法
虛擬機可以對滿足上述3個條件的無用類進行回收期丰。。吃挑。
2 垃圾收集算法
2.1 標記-清除算法
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象钝荡,在標記完成后統(tǒng)一回收所有被標記的對象。
不足:效率問題舶衬,標記和清除兩個過程的效率都不高埠通;空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片逛犹,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時端辱,無法找到足夠連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作
2.2 復(fù)制算法
將可用內(nèi)存按容量劃分為大小相等的兩塊梁剔,每次只使用其中的一塊,將這一塊的內(nèi)存用完了舞蔽,就將還存活著的對象復(fù)制到另外一塊上面荣病,然后再把已使用過的內(nèi)存空間一次清理掉。
優(yōu)點:這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收喷鸽,內(nèi)存分配時也不用考慮內(nèi)存碎片等復(fù)雜情況众雷,只要移動堆頂指針灸拍,按順序分配內(nèi)存即可做祝,實現(xiàn)簡單,運行高效鸡岗。
缺點:將內(nèi)存縮小為原來的一半
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代:
將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間混槐,每次使用Eden和其中一塊Survivor [1] 。當回收時轩性,將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一塊Survivor空間上声登,最后清理掉Eden和剛才用過的Survivor空間
2.3 標記-整理算法
標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理揣苏,而是讓所有存活的對象都向一端移動悯嗓,然后直接清理掉端邊界以外的內(nèi)存
2.4 分代收集算法
當前商業(yè)虛擬機的垃圾收集都采用“分代收集”(Generational? Collection)算法,這種算法并沒有什么新的思想卸察,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊
一般是把Java堆分為新生代和老年代脯厨,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?/p>
在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去坑质,只有少量存活合武,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集涡扼。
而老年代中因為對象存活率高稼跳、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收
3 HotSpot的算法實現(xiàn)
3.1 枚舉根節(jié)點
當執(zhí)行系統(tǒng)停頓下來后吃沪,并不需要一個不漏地檢查完所有執(zhí)行上下文和全局的引用位置汤善,虛擬機應(yīng)當是有辦法直接得知哪些地方存放著對象引用。在HotSpot的實現(xiàn)中票彪,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達到這個目的的萎津,在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來抹镊,在JIT編譯過程中锉屈,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣垮耳,GC在掃描時就可以直接得知這些信息了颈渊。
3.2 安全點
HotSpot也的確沒有為每條指令都生成OopMap遂黍,前面已經(jīng)提到,只是在“特定的位置”記錄了這些信息俊嗽,這些位置稱為安全點(Safepoint)雾家,即程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停绍豁。
安全點的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的——因為每條指令執(zhí)行的時間都非常短暫芯咧,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執(zhí)行”的最明顯特征就是指令序列復(fù)用竹揍,例如方法調(diào)用敬飒、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等芬位,所以具有這些功能的指令才會產(chǎn)生Safepoint无拗。
對于Sefepoint,另一個需要考慮的問題是如何在GC發(fā)生時讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點上再停頓下來昧碉。這里有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)英染,
搶先式中斷:不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時被饿,首先把所有線程全部中斷四康,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復(fù)線程狭握,讓它“跑”到安全點上闪金。
主動式中斷:當GC需要中斷線程的時候,不直接對線程操作哥牍,僅僅簡單地設(shè)置一個標志毕泌,各個線程執(zhí)行時主動去輪詢這個標志,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起嗅辣。輪詢標志的地方和安全點是重合的撼泛,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方
3.3 安全區(qū)域
需要安全區(qū)域的原因:
所謂的程序不執(zhí)行就是沒有分配CPU時間,典型的例子就是線程處于Sleep狀態(tài)或者Blocked狀態(tài)澡谭,這時候線程無法響應(yīng)JVM的中斷請求愿题,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間蛙奖。
安全區(qū)域:指在一段代碼片段之中潘酗,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中的任意地方開始GC都是安全的
4 垃圾收集器
收集算法是內(nèi)存回收的方法論雁仲,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)
5 內(nèi)存分配與回收策略
Java技術(shù)體系中所提倡的自動內(nèi)存管理最終可以歸結(jié)為自動化地解決了兩個問題:給對象分配內(nèi)存以及回收分配給對象的內(nèi)存
對象的內(nèi)存分配仔夺,往大方向講,就是在堆上分配(但也可能經(jīng)過JIT編譯后被拆散為標量類型并間接地棧上分配 [1] )攒砖,對象主要分配在新生代的Eden區(qū)上缸兔,如果啟動了本地線程分配緩沖日裙,將按線程優(yōu)先在TLAB上分配。
5.1 對象優(yōu)先分配在Eden分配
大多數(shù)情況下惰蜜,對象在新生代Eden區(qū)中分配昂拂。當Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次Minor GC抛猖。
新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動作格侯,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁财著,一般回收速度也比較快联四。
老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC瓢宦,經(jīng)常會伴隨至少一次的Minor GC(但非絕對的碎连,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)灰羽。Major GC的速度一般會比Minor GC慢10倍以上驮履。
5.2 大對象直接進入老年代
所謂的大對象是指,需要大量連續(xù)內(nèi)存空間的Java對象廉嚼,最典型的大對象就是那種很長的字符串以及數(shù)組(筆者列出的例子中的byte[]數(shù)組就是典型的大對象)
5.3 長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡(Age)計數(shù)器玫镐。如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話怠噪,將被移動到Survivor空間中恐似,并且對象年齡設(shè)為1。對象在Survivor區(qū)中每“熬過”一次Minor GC傍念,年齡就增加1歲矫夷,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中憋槐。
5.4 動態(tài)對象年齡判定
虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代双藕,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代阳仔,無須等到MaxTenuringThreshold中要求的年齡忧陪。
5.5 空間分配擔保
在發(fā)生Minor GC之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間近范,如果這個條件成立嘶摊,那么Minor GC可以確保是安全的。如果不成立评矩,則虛擬機會查看HandlePromotionFailure設(shè)置值是否允許擔保失敗叶堆。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小斥杜,如果大于虱颗,將嘗試著進行一次Minor GC俯萌,盡管這次Minor GC是有風險的;如果小于上枕,或者HandlePromotionFailure設(shè)置不允許冒險咐熙,那這時也要改為進行一次Full GC