github上的地址:DevelopBlog
與C語言不同懂酱,Java內(nèi)存(堆內(nèi)存)的回收由JVM垃圾收集器自動完成,不需要程序開發(fā)者手動釋放內(nèi)存曙求。
從Java內(nèi)存模型(鏈接)一文中,我們知道,java中幾乎所有的對象實例存儲在堆內(nèi)存中聘鳞,故而堆內(nèi)存是JVM垃圾回收的主要陣地。
哪些對象需要被回收要拂?
在討論GC之前我們需要考慮一個問題抠璃?如何確定一個對象是否需要被回收?
兩個方法:引用計數(shù)法
和可達性分析法
脱惰。
引用計數(shù)法
給對象添加一個引用計數(shù)器搏嗡,每當有一個地方引用它時,計數(shù)器+1拉一,當引用失效時采盒,計數(shù)器-1;當計數(shù)器為0舅踪,則表明它沒有被引用纽甘,也就是說可以被GC回收。
優(yōu)點:此方法簡單粗暴抽碌,效率很高悍赢。很多其他語言也是用這一方法進行對象的回收判斷决瞳。
缺點:此方法無法解決對象之間的相互循環(huán)引用問題。例如:
public class GCVinctorTest {
private Object instance = null;
public static void main(String[] args) {
GCVinctorTest objA = new GCVinctorTest();
GCVinctorTest objB = new GCVinctorTest();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
- step1:實例A的引用計數(shù)加1左权,實例A的引用計數(shù)=1皮胡;
- step2:實例B的引用計數(shù)加1,實例B的引用計數(shù)=1赏迟;
- step3:實例A的引用計數(shù)加1屡贺,實例A的引用計數(shù)=2;
- step4:實例B的引用計數(shù)加1锌杀,實例B的引用計數(shù)=2甩栈;
執(zhí)行
objA = null;
objB = null;
之后:
- 實例A的引用計數(shù)減1,實例A的引用計數(shù)=1糕再;
- 實例B的引用計數(shù)減1枉长,實例B的引用計數(shù)=1离熏;
此時开镣,objA與objB相互引用哗咆,他們的引用計數(shù)器都不是0,除此之外猾担,再無任何其他實際引用袭灯,但是引用計數(shù)法無法通知GC收集器回收他們。
可達性分析算法
可達性分析算法 通過一系列的被稱為GC Roots
的對象愛你個作為起始點(相當于根)绑嘹,其他對象(相當于樹枝或樹葉)直接或間接都與這個GC Roots
相連稽荧。如果一個對象與root不相連,則就說明這個對象是不可用的圾叼,GC就可以將它回收蛤克。如圖所示
圖中obj1,obj2,obj3都與roots有直接或間接關聯(lián),不可被回收的存活對象夷蚊,obj6,obj4髓介,obj5雖然這三者之間有關聯(lián)惕鼓,但是與roots已經(jīng)斷開,故而可被回收對象唐础。
在HotSpot
中箱歧,JVM使用OopMap
的數(shù)據(jù)結構來記錄對象內(nèi)什么偏移量存儲的是什么類型的數(shù)據(jù)的映射關系,在JIT編譯過程中一膨,也會在特定位置記錄下棧和寄存器中的那些位置和引用的呀邢,這樣GC在掃描時就可以直接獲得這些信息,而不需要去遍歷GC roots的引用鏈豹绪,提高了回收效率价淌。
在HotSpot
中,使用可達性分析算法進行可回收對象的標記。
GC ROOTS分類:
* 虛擬機棧(棧幀中的本地變量表)中引用的對象
* 方法區(qū)中類靜態(tài)屬性引用的對象
* 方法區(qū)中常量引用的對象
* 本地方法棧中JNI(Natice方法)中引用的對象
(finalize()方法不討論)
從從上圖看出蝉衣,存在3個GC Root:
靜態(tài)變量RefV1
指向堆中實例1括尸;
局部變量RefV2
指向堆中實例2;
Jni變量RefV3
指向堆中實例3病毡,實例3指向了實例4濒翻;
故而,實例1啦膜,2有送,3,4都是可以存活的對象僧家;
而實例5不存在GC Root雀摘,故實例5可以被回收,雖然實例6被實例5引用啸臀,但是實例5沒有Gc Root(整條鏈無Root)届宠,故實例6也可以被回收。
引用的四種類型:
從強到弱分為:強引用乘粒,軟引用(soft)豌注,弱引用(weak),虛引用
- 強引用:在開發(fā)中經(jīng)常的寫法灯萍,類似于
BeanDemo demo=new BeanDemo();
這種寫法轧铁,極為強引用。只要這種引用還存在旦棉,該實例就不會被標記為可回收齿风,垃圾回收器也就不會回收掉該對象。 - 軟引用:用來引用一些有用但是非必需的對象绑洛。
在系統(tǒng)將要發(fā)生內(nèi)存溢出OOM時
救斑,有這種引用的對象,將要被回收真屯。 - 弱引用:用來引用一些有用但是非必需的對象脸候。但是與
軟引用在OOM進行回收
不同,有這些引用的對象绑蔫,只要發(fā)生垃圾回收运沦,該對象將被回收。 - 虛引用:無法通過虛引用獲得一個對象的實例配深,設置虛引用的目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知携添。
垃圾回收算法
標記—清除算法(mark-and-sweep)
顧名思義:先標記后清除(廢話)。JVM首先標記出所有需要回收的對象篓叶,在標記完成之后烈掠,在下一次垃圾回收的時候統(tǒng)一進行回收這些對象羞秤。
如圖所示:
清除前
清除后
可以看到,垃圾回收之后向叉,剩余的存貨對象分布比較雜亂锥腻,產(chǎn)生大量的碎片。碎片如果太多母谎,將會導致瘦黑,內(nèi)存空間的不連續(xù)性,如果這時出現(xiàn)一個大對象(如數(shù)組)奇唤,將會無法為其分配空間幸斥。
復制算法(Copying)
為了解決標記—清除算法
的弊端,出現(xiàn)了復制算法
咬扇。該算法首先將內(nèi)存空間分為兩部分甲葬,一次只使用其中的一塊。當GC發(fā)生之時懈贺,對象被清理之后经窖,將剩余的存活對象復制到另一部分內(nèi)存空間上,再把當前的內(nèi)存空間清空梭灿,這樣就解決了碎片過多的問題画侣。如圖所示:
此方法雖然解決了碎片過多的問題,但是另一個顯著問題又出現(xiàn)了:內(nèi)存利用率太低堡妒!
在同一時間配乱,只有一部分的內(nèi)存在被使用,如果對象存活率比較高的時候皮迟,復制算法將會進行較高的復制操作搬泥,復制算法將會非常低》幔考慮上篇文章的老年代與新生代的各自不同特點忿檩,可見此算法不適用于老年代。
標記—整理算法
為了解決復制算法的利用率低的問題爆阶,提出了標記—整理算法
算法休溶,與標記—清除算法
一樣,首先對對象進行標記扰她,但后續(xù)步驟則不是對可回收對象進行清理,而是讓存活的對象向內(nèi)存空間的一端就行移動芭碍,然后清理掉端邊界以外的內(nèi)存徒役。
分代收集
這是上篇文章已經(jīng)提及的,根據(jù)對象的存活周期將內(nèi)存劃分為新生代與老年代窖壕。然后根據(jù)各個年代不同的特點采用最適合的手機算法忧勿。
例如:
- 新生代的對象
朝生夕死
杉女,時時產(chǎn)生新對象,時時又會有大批對象死去鸳吸,這個區(qū)域就可以使用復制算法熏挎; - 老年代因為對象存活率特別高,不容易被回收晌砾,需要使用
標記—清除
或標記—整理
算法進行回收坎拐。
上文中提到,在GC過程中使用OopMap
記錄對象 的偏移和類型信息养匈,隨著程序的執(zhí)行哼勇,無時不刻都有可能會產(chǎn)生新的對象,如果每進行一步操作呕乎,都生成相應的OopMap
积担,那會需要大量的額外空間,GC成本變的非常高猬仁。在實際的HotSpot
中帝璧,JVM只在程序執(zhí)行到特定的位置才生成OopMap
,這些位置稱為“安全點
”湿刽。
當程序執(zhí)行到安全點之后的烁,由于需要進行GC ROOTS統(tǒng)計,需要暫停進程中所有的線程叭爱,如果不暫停撮躁,就會在統(tǒng)計的過程中不斷產(chǎn)生的新的對象,使得統(tǒng)計無法得到準確的結果买雾。
安全點的選定既不能太少把曼,會導致GC等待時間太長;又不能太多漓穿,導致太過于頻繁而增大運行負荷嗤军。故而選定的標準為:是否有讓程序城市間執(zhí)行的特征。一般最明顯的就是制定序列的服用(指令將在以后介紹)晃危,如:方法調(diào)用叙赚,循環(huán)跳轉(zhuǎn),異常跳轉(zhuǎn)等僚饭。
線程如何達到安全點呢震叮?JVM在安全點的地方設置一個中斷標志,當線程執(zhí)行到這個標志時鳍鸵,線程自己中斷掛起苇瓣。
這里還有一個問題,當一個線程沒有處于運行狀態(tài)的時候偿乖,如SLeep或者Blocked狀態(tài)击罪,那么如何才能中斷自身呢哲嘲?GC也不可能等待該線程重新?lián)屨糃PU再進行中斷,這時安全區(qū)域
的概念產(chǎn)生了媳禁。安全區(qū)域同安全點的概念一樣眠副,只不過安全區(qū)域是一段代碼片段。
上文提過竣稽,為了保證GC ROOTS統(tǒng)計的完整性囱怕,統(tǒng)計時產(chǎn)生新的引用關系,才提出的安全點這一概念丧枪。同樣光涂,我們選定安全區(qū)域的標準也是該段代碼并不會產(chǎn)生新的引用關系
。
在線程執(zhí)行到安全區(qū)域中的代碼時拧烦,首先標記一下自己已經(jīng)進入了安全地帶了忘闻,這樣JVM進行GC時,就不用考慮該線程的引用關系了恋博。當線程將要離開
該安全區(qū)域的時候齐佳,該線程首先檢查自己是否已經(jīng)完成了GC ROOTS的統(tǒng)計枚舉,如果完成了债沮,那就繼續(xù)往下執(zhí)行代碼炼吴,并將之前進入安全區(qū)域的標識去掉;如果沒有完成GC ROOTS的枚舉疫衩,則該線程中斷硅蹦、等待,直到收到GC ROOTS枚舉已經(jīng)統(tǒng)計完成的信號闷煤,才可以繼續(xù)執(zhí)行下面的代碼童芹。
就醬,本文介紹了GC如何識別出那些需要回收和存活下來的對象鲤拿,并從理論上介紹了GC的回收算法假褪,下篇文章將詳細介紹JVM中的一些具體的垃圾回收器。
(部分圖片源于網(wǎng)絡近顷,如果侵犯到您的權益生音,請聯(lián)系本人刪除。)
著作權歸作者所有窒升。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權缀遍,非商業(yè)轉(zhuǎn)載請注明出處。