所謂垃圾回收,也就是要回收已經“死了”的對象。
那我們如何判斷哪些對象“存活”,哪些已經“死去”呢?
一、判斷對象已死
1、引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加一;當引用失效時,計數器就減1;任何時刻計數器為0的對象就是不可能再被使用的。
但是在Java虛擬機里面沒有選用引用計數算法來管理內存。
優點:實現簡單,效率高。
缺點:很難解決對象之間相互循環引用的問題。
2、可達性分析算法
通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
如圖,object5、object6、object7 為可回收對象
主流的Java虛擬機使用可達性分析算法
關于GC Roots:
在Java語言中,GC Roots包括以下幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中Native方法引用的對象
無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可 達,判定對象是否存活都和“引用”離不開關系。
引用分為四種:
- 強引用:代碼中普遍存在,垃圾收集器不會回收強引用的對象。比如new Object()
- 軟引用:有用但非必需,在系統將要發生內存溢出異常OOM之前,會把這些對象列入回收范圍,進行回收。
- 弱引用:非必需,無論當前內存是否足夠,下次垃圾回收都會回收掉這些對象。
- 虛引用:最弱的引用關系,是否有虛引用不對其生存時間構成影響。相當于什么時候回收都沒問題,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知
二、三種垃圾回收算法
1、標記-清除算法
最基礎的收集算法——“標記-清除”(Mark-Sweep)算法。一般用于老年代
算法分為“標記”和“清除”兩個階段:
- 標記出需要回收的對象
- 清除被標記的對象
缺點:
- 當有大量對象等待被回收,此時就需要大量的標記和清除操作,導致兩個過程的效率隨對象數量增長而降低,執行效率不穩定
- 標記,清除后會產生大量不連續的內存碎片,導致空間碎片化問題
2、標記-復制算法
為了解決效率問題,標記-復制算法出現了。他將內存分為兩塊,每次只使用其中一塊,當一塊內存用完了,就將還存活的對象復制到另一塊上面,然后把已經使用過的內存空間一次性清理掉。
缺點:內存縮小為原來的一半。
但是在新生代的內存劃分中,研究表明,有98%的對象熬不過第一輪收集,因此沒必要采用1:1的內存劃分。針對這種情況,產生了半區分代策略。是把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾搜集時,將Eden和Survivor中仍 然存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空 間。HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1?。如果另外一塊 Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象,這些對象便將通過分配擔保機制直 接進入老年代,這對虛擬機來說就是安全的。
3、標記-整理算法
在Mark-Sweep算法的基礎上做了改良,用于解決空間碎片化問題。標記-整理(Mark-Compact)算法在標記后不是簡單做清除,而是讓所有存活的對象都向一端移動,然后清理掉端邊界以外的內存。一般用于老年代。
優點:解決了空間碎片化問題,為后續內存分配和訪問提高效率
缺點:使內存回收的過程更加復雜。如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象并更新所有引用這些對象的地方必須全程暫停用戶應用程序才能進行。
三、安全點和安全區域
安全點
在做可達性分析時,需要保持分析期間整個系統不會發生變化,這就導致GC進行時必須停頓所有Java執行線程(Stop The World),即使是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也必須要停頓。
程序執行時并非在所有地方都能停下來開始GC,只有在到達安全點(Safepoint)時才能暫停。Safepoint 的選定既不能太少以致于讓GC等待時間太長,也不能過于頻繁以致于過分增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的,例如方法調用,循環跳轉,異常跳轉等。
如何在GC發生時讓線程都跑到最近的安全點再停頓下來?
- 搶先試中斷:先把所有線程中斷,發現不在安全點的線程恢復線程,讓它跑到安全點。
- 主動式中斷:設置一個不可讀的內存位置作為中斷標志,標志與安全點重合,當線程執行到這個標志時自己中斷掛起。
安全區域
安全區域(Safe Region)是指在一段代碼片段中,引用關系不會發生變化。在這個區域的任何地方開始GC都是安全的。典型的安全區域比如線程處于Sleep狀態或者Blocked狀態。
在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region。當要發起GC時,就不用管標識為Safe Region狀態的線程了。當線程要離開Safe Region時,要檢查是否處于GC狀態,如果是,就要繼續等待,直到收到可以安全離開Safe Region的信號為止。