前言
說到GC,一直沒有系統(tǒng)的看過允蚣。但是于颖,很顯然,是一個非常經(jīng)典的知識點嚷兔。毫不夸張的說森渐,面試的時候問道GC而你一無所知的話,基本上是涼了冒晰。
今天拜讀了JVM經(jīng)典書《深入理解Java虛擬機》同衣,對于GC講的很詳細。書中在第三章講的是垃圾收集器與內(nèi)存分配策略壶运,我準備分三篇文章來記錄這章的讀書筆記耐齐,本篇介紹一下垃圾回收流程,第二篇介紹垃圾收集器,第三篇介紹新老生代的劃分以及內(nèi)存分配策略蚪缀。本篇更偏向于理論和算法思想秫逝,所以看起來還是很有趣的。值得注意的是询枚,我們只有在程序處于運行期間才知道創(chuàng)建了哪些對象违帆,這部分內(nèi)存的分配和回收是動態(tài)的,垃圾收集關(guān)注的也是這部分內(nèi)存金蜀。學習GC通俗來說也就三個問題:
哪些內(nèi)存需要回收刷后?
什么時候回收?
如何回收
下面渊抄,我們也根據(jù)這三個問題來逐步了解GC尝胆。
對象占用內(nèi)存护桦,所以不再使用的對象所占用的內(nèi)存需要回收含衔。那么,怎么判斷對象不再使用呢二庵?判斷對象是否 “死亡” 有以下方法:
引用計數(shù)算法
給對象中添加一個引用計數(shù)器贪染,每當有一個地方引用它時,計數(shù)器值就加1催享;當引用失效時杭隙,計數(shù)器值就減1;任何時刻計數(shù)器都為0的對象就是不可能在被使用的因妙。引用計數(shù)算法實現(xiàn)簡單痰憎,判斷效率也很高,在大部分情況下都是一個不錯的算法攀涵。但是铣耘,在Java中并沒有選擇引用計數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間的相互循環(huán)引用的問題汁果。
根搜索算法
Java中涡拘,是用根搜索算法來判斷對象是否存活的玲躯。這個算法的思路就是通過一系列的名為 “GC Roots” 的對象作為起始點据德,從這個節(jié)點開始向下搜索,搜索所有走過的路徑稱為引用鏈(Reference Chain)跷车,當一個對象到了GC Roots沒有任何引用鏈相連的時候(不可達)棘利,則說明此對象是不可用的,被判定為可回收對象朽缴。
一圖勝千言:(圖是槍來的……)
根據(jù)引用判斷
無論是通過引用計數(shù)算法判斷對象的引用數(shù)量善玫,還是通過根搜索算法判斷對象的引用鏈是否可達,判斷對象是否存活都與引用有關(guān)密强。
強引用(Strong Reference)
在代碼中普遍存在茅郎,類似 Object obj = new Object() 蜗元。只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象系冗。
軟引用(Soft Reference)
用來描述一些還有用奕扣,但并非必須的對象。對于軟引用關(guān)聯(lián)的對象掌敬,在系統(tǒng)將要發(fā)生OOM異常之前惯豆,將會把這些對象列進回收范圍之中并進行第二次回收。如果這次回收還是沒有足夠的內(nèi)存奔害,才會拋出內(nèi)存溢出異常楷兽。在JDK1.2之后,提供了SoftReference類實現(xiàn)軟引用华临。
弱引用(Weak Reference)
也是用來描述非必須對象芯杀,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前雅潭。當垃圾收集器工作時瘪匿,無論當前內(nèi)存是否足夠,都會回收掉只有被弱引用關(guān)聯(lián)的對象寻馏。在JDK1.2之后棋弥,提供了WeakReference類實現(xiàn)弱引用。
虛引用(Phantom Reference)
它是最弱的一種引用關(guān)系诚欠。一個對象是否有虛引用的存在顽染,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例轰绵。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是希望能在這個對象被收集器回收時收到一個系統(tǒng)通知粉寞。在JDK1.2之后,提供了PhantomReference類來實現(xiàn)虛引用左腔。
當一個對象不可達時,并不是馬上就會被回收的液样。這里就要說一下finalize()方法振亮,這個方法被調(diào)用有三種情況:
所有對象被CG時自動調(diào)用,比如運行System.gc()的時候
程序退出時為每個對象調(diào)用一次finalize方法
顯式的調(diào)用finalize方法
再盜一張圖:
JVM 能夠保證一個對象在回收以前一定會調(diào)用一次它的finalize()方法鞭莽。
需要注意的是坊秸,你永遠不知道它什么時候被調(diào)用甚至會不會調(diào)用,因為有些對象永遠不會被回收的澎怒,或者被回收以前程序就已經(jīng)結(jié)束了褒搔。但是如果它有必要執(zhí)行finalize()的,那么在GC前一定調(diào)用一次且僅且一次,如果在第一次GC時沒有被回收星瘾,那么以后在GC時就不會在調(diào)用finalize()走孽。
那就要說說回收算法啦琳状。
標記清除算法(Mark-Sweep)
算法分為標記和清除兩個階段融求,首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收掉被標記的對象算撮。
缺點:
效率低下生宛,標記和清除的效率都不高
空間問題,標記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片肮柜,空間碎片太多可能會導致需要分配大對象無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集陷舅。
復制算法(Copying)
它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊审洞。當這一塊的內(nèi)存用完了莱睁,就將還存活的對象復制到另外一塊上面,然后再把已經(jīng)使用過的內(nèi)存空間一次清理掉芒澜。這樣使得每次都是對其中的一塊進行內(nèi)存回收仰剿,內(nèi)存分配也不用考慮內(nèi)存碎片等復雜問題,只要移動堆頂指針痴晦,按順序分配內(nèi)存即可南吮,實現(xiàn)簡單,運行高效誊酌。
當然部凑,缺點也很明顯,那就是要犧牲一半的內(nèi)存代價碧浊。
但是事實上涂邀,并不需要按1:1劃分殉挽,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間鹃锈,每次使用Eden和其中一塊Survivor。當回收時州叠,將Eden和Survivor空間還存活的對象一次性的復制到另外一塊Survivor空間上驹止,最后清理掉Eden和剛剛用過的Survivor空間浩聋。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1。但是幢哨,當對象存活率較高時就要執(zhí)行較多的復制操作赡勘,效率將會變低嫂便。
標記-整理算法(Mark-Compact)
此算法結(jié)合了標記清除和復制兩個算法的優(yōu)點捞镰,分為兩個階段:
第一階段從根節(jié)點開始標記所有被引用的對象
第二階段遍歷整個堆,把清除未標記對象并且把存活的對象壓縮到堆的其中一塊,按順序排放岸售。此算法避免了標記清除的碎片問題践樱,同時也避免了復制算法的空間問題。
分代收集算法(Generational Collection)
當前商業(yè)虛擬機的垃圾回收都采用分代收集算法凸丸,這種算法并沒有什么新的思想拷邢,只是根據(jù)對象的存活周期將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代屎慢,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴út稼。在新生代中,每次垃圾回收都有大批對象死去腻惠,只有少量存活环肘,那就選用復制算法,只要付出少量存活對象的復制成本就可以完成收集集灌。而老年代中因為對象存活率高悔雹,沒有額外的空間對它進行分配擔保,那就使用標記清理或者標記整理算法來進行回收欣喧。