Java 使用了垃圾收集器來代替手動管理內存,對於垃圾收集器來講,不管哪一種,其核心思想都是作兩件事:算法
最直接,最容易想到的標記方法是引用計數法,顧明思議,記錄每一個對象被引用的個數,若是爲0,則爲死亡對象。該方法實現簡單,判斷效率高,但很難解決對象之間相互循環引用的問題。 post
在JVM中使用了可達性分析計算的方式來標記存活對象,GC 定義了一些特殊的對象做爲 GC Roots:優化
以 GC Roots 做爲起始點,沿着引用路徑不斷搜索,同時標記搜索到的對象爲存活。 線程
注意,在標記階段,須要中止應用線程(Stop the World),由於沒有辦法在應用程序不斷改變引用關係的同時一邊標記。暫停的時間取決於存活對象的多少,存活的對象越多,須要標記的時間越長。3d
在學術上,標記清除算法是最具表明性的算法:cdn
Marking: 經過 GC Roots 開始搜索標記可達的對象對象
Sweeping: 使得被未標記的對象佔用的內存空間能夠被以後分配使用blog
可是這樣直接 Sweep 會存在兩個問題:圖片
爲了不這個問題,還須要作一次碎片整理 內存
還有一種更簡單的方法,將內存分爲兩塊,每次只使用其中的一塊區域,發生 GC 時,將存活的對象複製到另外一個區域中,這樣也不會出現碎片的問題,此外,複製能夠在標記的同時進行,更加高效。缺點也很明顯,須要更多的內存。這種算法被稱爲標記-複製算法。
研究人員觀察到,應用程序內的大多數分配分爲兩類:
基於這個假設,虛擬機將內存分爲了兩個代,分別爲 新生代(Young Generation), 和老年代(Old Generation or Tenured)。
那麼針對不一樣代的特色,能夠有針對性的進行算法優化,通常來講將算法分爲 Mionr GC(只回收新生代對象)和 Full GC(全局回收)。 這個假設也存在兩個問題:
一般狀況下 Eden 是對象建立時被分配的區域。因爲涉及到多個線程同時建立對象,Eden 被劃分紅了一個或多個 Thread Local Allocation Buffer (TLAB) ,簡單來講,每一個線程都被分配了一塊區域用於本線程的對象分配(避免的線程同步代價),若是分配的內存不夠使用了,則使用共有的部分(申請新的TLAB),若是再不夠,則觸發一次新生代 GC(Minor GC) ,若是清理後的內存仍然不夠,則將對象分配在老年代。
在 Mionr GC 時,首先經過 GC Roots 掃描標記全部存活的對象,須要注意以前提到過,老年代的對象也有可能引用新生代對象。對於這個問題,JVM 使用了 card-marking 來避免老年代的掃描。HotSpot 使用了卡表(Card Table)的技術,將整個堆劃分爲一個個大小爲512字節的卡,若是卡中的對象可能指向新生代對象引用,那麼這張卡是髒的,同時 JVM 維護了一個卡表,每張卡都有一個對應的標識位來表示是不是髒卡。那麼在進行 Minor GC 時,只需將髒卡中的對象將入到 GC Roots 裏,而不用掃描整個老年代。
完成標記後,將全部存活的對象複製到其中一個 Survivor 區中,此後整個 Eden 區的內存均可以從新被使用了。這個算法也叫作「標記-複製「算法(Mark and Copy)。 Survivor 分爲 from 和 to 兩個區域(每次GC後身份互換),其中 to 區域永遠是空的,當GC完成標記後,Eden 和 from 區域的存活對象都複製到 to 區域中,from區域清空,兩個區域身份互換。
對象可能在兩個 Survivor 中不斷的來回複製,當複製達到必定次數時(默認15次),將被認爲足夠老,晉升到老年代中。此外,若是 Survivor 區域大小不夠存放全部存活對象,則會將較老的對象提早晉升到老年代中。
老年代內存要大的多,而且大多數對象都不會是垃圾,而且發生GC的頻率要相對小的多,因此複製算法不適用。通常來講,使用「標記-清除-整理「的算法對老年代進行回收。
至於 JVM 是如如何使用這些算法實現具體的回收器的,請看這篇 通俗易懂 JVM 中的 GC 實現
圖片來源及參考資料:《Plumbr Handbook Java Garbage Collection》