1. 程式人生 > 其它 >深入瞭解JVM垃圾收集器

深入瞭解JVM垃圾收集器

程式計數器、JVM棧、本地方法棧這三個記憶體區域和執行緒是一一對應的,並且每一個執行緒的這三個區域相互獨立互不干擾。他們都隨著執行緒的產生而產生,執行緒的滅亡而滅亡。JVM棧和本地方法棧中的棧幀都隨著方法的載入而入棧,隨著方法的結束而出棧。

棧幀的大小是在程式設計師寫類的時候就確定下來的。因此這三種區域的記憶體大小都具備確定性,當方法結束或執行緒結束後,這些記憶體空間就會自動被回收掉,所以JVM無需考慮這些區域的記憶體回收問題。

堆記憶體和方法區的記憶體分配和回收就不一樣了,因為一個介面中的多個實現類所需要的記憶體可能不一樣,並且一個方法中的多個分支所需要的記憶體也不一樣,到底要執行哪個分支,這就要看執行時的情況了。所以堆記憶體中的資料的分配和回收都是動態的。所以垃圾回收器只關注堆記憶體中的資料的分配和回收。

判斷物件是否已死的理論方法

  • 引用計數演算法 a)引用計數演算法是什麼? 給所有的物件新增一個引用計數器,每當有一個地方引用了這個物件時,就將計數器的值+1;每當引用失效時,就將計數器的值-1;當一個物件計數器的值為0時,就認為這個物件已經沒用了,垃圾收集器可以把它回收。 b)引用計數演算法的優點 這個演算法實現簡單,效率較高,大部分情況下都是個不錯的演算法。 c)引用計數演算法的缺點 Java並沒有選用引用計數演算法來管理記憶體,因為這個演算法當遇上物件的迴圈引用的時候就黔驢技窮了。 d)啥是“迴圈引用“? 這個程式中,首先建立兩個有引用指向的物件obj1、obj2; 然後分別將堆記憶體中兩個物件中的obj0引用分別指向物件,這樣,堆記憶體中的兩個物件互相指向; 最後去除JVM棧中兩個引用對堆記憶體中兩個物件的指向;此時堆記憶體中的兩個物件已經脫離了JVM棧中引用的指向,但堆記憶體中的兩個物件互相指向,所以這兩個物件的計數器都不為0,所以垃圾收集器不會將這兩個物件視作垃圾。但事實上,只要堆記憶體中的物件沒有JVM棧中引用指向的時候,這些物件就已經沒有用了,因為函式已經無法再操控這些沒有引用指向的物件了。這種迴圈引用的物件也應當被當作垃圾回收。
public class A{
    public Object obj0 = null;
    public static void main(String[] argus){
        A obj1 = new A();
        A obj2 = new A();
        obj1.obj0 = obj2;
        obj2.obj0 = obj1;
        obj1 = null;
        obj2 = null;
    }
}
  • 根搜尋演算法 a)特點? 目前主流的程式語言都是使用根搜素演算法來判定物件是否存活的,Java也是。 b)是啥? 根搜尋演算法中將GC Roots作為起始節點,然後垃圾回收器從這這些起始節點開始搜尋,搜尋走過的路經稱為引用鏈,當一個物件到GC Roots不可達,則證明這個物件已經死了!垃圾收集器可以將它回收。 c)GC Roots由以下幾種物件構成 1.存放在JVM棧中的棧幀中的本地變量表中的 引用的 物件可作為GC Roots 2.存放在方法區中的靜態屬性引用的物件,可作為GC Roots 3.方法區中的常量引用的物件可作為GC Roots 4.本地方法棧中的JNI(即Native方法)的引用的物件,可作為GC Roots

Java的四種引用

  • 強引用 我們平常建立的物件都是強引用:Person p = new Person(); 強引用只要JVM棧中引用對應堆記憶體中的物件指標存在,那麼GC就不會回收堆記憶體中的這個物件。
  • 軟引用 通過SoftReference類將物件宣告成軟引用之後,一般情況下,堆記憶體中的一個物件被JVM棧中的一個軟引用指向,那麼在垃圾回收的時候這個物件是不會被回收的;當堆記憶體即將發生OOM異常,此時JVM就會做出一次假設:假設堆記憶體中所有被軟引用指向的物件被回收了,是否還會發生OOM,如果不會發生OOM,那麼就將所有軟引用的物件回收;如果還會發生OOM,那麼就直接丟擲OOM。
  • 弱引用 被弱引用指向的物件,它們的生命週期只有從建立到發生一次垃圾回收結束;也就是說,一旦發生了垃圾回收,這些被弱引用指向的物件一定會被殺死! 通過WeakReference類來宣告弱引用。
  • 虛引用 也叫幽靈引用或幻影引用,虛引用雖然也指向了一個堆記憶體中的物件,但無法通過這個引用來對這個物件做任何的處理。一個物件使用虛引用關聯的唯一目的就是希望這個物件在被垃圾收集器回收前收到一個系統通知。 虛引用使用PhantomReference類來宣告。

物件的回收過程

  1. 首先,垃圾收集器尋找所有與GC Roots沒有關聯的物件;
  2. 檢視這些物件是否重寫了finalize方法,或finalize方法是否已被執行過;
  3. 若這個物件沒有重寫finalize方法,或finalize方法已經被執行過了,那麼這個物件就被殺死了;
  4. 如果重寫了finalize方法,那麼執行這個方法,如果這個方法執行完後,這個物件與GC Roots發生了關聯,那麼這個物件就逃過一劫,否則這個物件就被殺死。 PS:Java提供finalize方法的目的是為了讓那些C++程式設計師適應Java中不需要物件的手動釋放,這個方法中可以做一些物件生命結束時的善後工作。但是我們平時千萬不要使用這個方法,因為它開銷很大,而且finalize方法能做的工作,try-finally也可以做。這裡只是為了瞭解finalize方法,它在實際開發中完全不需要!

方法區中資料的回收

方法區中存放的是類資訊、常量、靜態變數、編譯後的程式碼,這些資料一般需要長時間使用,所以方法區中的垃圾回收的價效比比較低。在堆記憶體中進行垃圾回收,一次可以回收70%-95%的空間;而在方法區中進行垃圾回收的效率遠遠低於堆記憶體的回收效率。

方法區中的垃圾回收主要回收兩部分資料:廢棄常量、無用的類。

  • 廢棄常量的回收 廢棄常量的回收和堆記憶體中物件的回收非常類似,加入字串常量“abc“已經沒有任何一個String型別的引用指向,那麼當發生垃圾回收時,這個常量就會被記憶體回收。 PS:常量都儲存在方法區的常量池中,變數都儲存在JVM棧中的棧幀中的區域性變量表中。
  • 無用類的回收 相對於廢棄常量的回收,對於無用類的回收相對比較複雜。 一個類同時滿足以下這三個條件才能被判定為無用類: a)堆記憶體中不存在該類的任何例項 b)載入該類的ClassLoader已被回收 c)該類的Class物件沒有在任何地方被引用,也就是沒有任何地方通過反射機制訪問該類中的成員。 PS:每個類都對應著一個Class物件,記錄類的相關資訊。

垃圾回收演算法

  • 標記-清除演算法 a)是什麼? 這個演算法在執行過程中,首先會標記出需要清除的物件們, 標記完成後統一回收所有的物件。 b)缺點 回收和清除過程效率都不高。 清除之後會產生大量不連續的記憶體空間,當有一個大物件申請記憶體空間的時候,由於沒有連續的大記憶體分配給它,這時又不得不發起一次垃圾收集工作。
  • 複製演算法 a)是什麼? 為了提升標記-清除演算法的效率問題,所以出現了複製演算法。它將記憶體分成完全相同的兩塊,每次只使用其中的一塊,如果這一塊記憶體用完了,就進行一次垃圾清理,然後把還存活的物件複製到另一塊上去,然後把剛才的那塊記憶體全部清理乾淨。由於每次垃圾回收都將物件集中到另一塊上去,所以碎片就減少了很多。 b)特性 現在的商業虛擬機器都是採用這種收集演算法來回收新生代。 IBM的研究表明,新生代中的物件98%都是經歷了一次GC之後就會死掉,所以堆記憶體不必按照1:1的大小來劃分,而是將堆記憶體劃分成一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配記憶體時只使用一塊較大的Eden和Survivor。當進行垃圾回收時,大約98%的物件都被殺死了,再將Eden和Survivor空間中2%的那些活著的物件複製到另一個Survivor中,最後將Eden和Survivor中的物件全部清除。 HotSpot虛擬機器預設的Eden和Survivor的大小比是8:1,也就是Eden、Survivor、Survivor的大小比例是:80%、10%、10%,也就是隻有10%的空間會被浪費掉。 但是,HotSot預設的Eden和Survivor的大小比是8:1,但是我們沒有辦法保證經過一次GC之後能存活的物件總大小能裝入第二個Survivor中。當出現存活的物件無法裝入Survivor中時,此時就需要將存活的物件裝入老年代中,這種方法叫做分配擔保。 c)什麼是分配擔保? 一般情況下,第二個Survivor大小佔10%即可處理大部分GC情況,但有時候GC完一次之後可能存在超過10%的物件存活,此時這些物件裝不下第二個Survivor中,所以就需要老年代的空間作為發生這種情況時的擔保,將超過10%的物件裝入老年代中去。 d)缺點 若經過一次GC之後物件存活率很高,就需要大量的複製操作將物件複製到第二個Survivor中去,此時就會效率低。
  • 標記-整理演算法 和標記-清理演算法類似,首先將所有沒有引用指向的物件作標記,然後清理掉這些物件,最後將所有存活的標記移向一邊。這樣就減少了標記-清理演算法中的碎片問題。
  • 分代收集演算法 當前商業虛擬機器的垃圾收集都採用分代收集演算法,這種演算法根據物件存活時間的不同將記憶體分為幾塊,一般把堆記憶體分為新生代和老年代。在新生代中,每次垃圾回收都有大量的物件死去,只有少量的物件存活,就使用複製演算法,只需要付出少量的複製成本就可以完成垃圾收集工作。在老年代中的物件存活率高,而且沒有額外的空間給它進行分配擔保,所以必須使用標記-清除 或 標記-整理 演算法進行垃圾回收。