GC回收機制

什麼是垃圾

所謂垃圾就是內存中已經沒有用的對象。 既然是」垃圾回收",那就必須知道哪些對象是垃圾。Java 虛擬機中使用一種叫做"**可達性分析」**的算法來決定對象是否能夠被回收。算法

可達性分析

可達性分析算法是從離散數學中的圖論引入的,JVM 把內存中全部的對象之間的引用關係看做一張圖,經過一組名爲」GC Root"的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,最後經過判斷對象的引用鏈是否可達來決定對象是否能夠被回收。以下圖所示:數組

Cgq2xl58leGAIoKxAAEZWYE_v08477.png 好比上圖中,對象A/B/C/D/E 與 GC Root 之間都存在一條直接或者間接的引用鏈,這也表明它們與 GC Root 之間是可達的,所以它們是不能被 GC 回收掉的。而對象M和K雖然被對J 引用到,可是並不存在一條引用鏈鏈接它們與 GC Root,因此當 GC 進行垃圾回收時,只要遍歷到 J/K/M 這 3 個對象,就會將它們回收。markdown

注意:上圖中圓形圖標雖然標記的是對象,但實際上表明的是此對象在內存中的引用。包括GC Root也是一組引用而並不是對象。併發

GC Root對象

在Java中,有如下幾種對象能夠做爲GC Root:
1.Java 虛擬機棧(局部變量表)中的引用的對象。
2.方法區中靜態引用指向的對象。
3.仍處於存活狀態中的線程對象。
4.Native 方法中 JNI 引用的對象。性能

何時回收

1.Allocation Failure:在堆內存中分配時,若是由於可用剩餘空間不足致使對象內存分配失敗,這時系統會觸發一次 GC。
2.System.gc():在應用層,Java 開發工程師能夠主動調用此 API 來請求一次 GC。spa

垃圾回收算法

標記清除算法

從」GC Roots」集合開始,將內存整個遍歷一次,保留全部能夠被 GC Roots 直接或間接引用到的對象,而剩下的對象都看成垃圾對待並回收,過程分兩步。線程

  1. Mark 標記階段:找到內存中的全部 GC Root 對象,只要是和 GC Root 對象直接或者間接相連則標記爲灰色(也就是存活對象),不然標記爲黑色(也就是垃圾對象)。
  2. Sweep 清除階段:當遍歷完全部的 GC Root 以後,則將標記爲垃圾的對象直接清除。

標記清除.png

優勢:實現簡單,不須要將對象進行移動。
缺點:這個算法須要中斷進程內其餘組件的執行(stop the world),而且可能產生內存碎片,提升了垃圾回收的頻率。3d

複製算法

將現有的內存空間分爲兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中。以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。調試

  1. 複製算法以前,內存分爲 A/B 兩塊,而且當前只使用內存 A,內存的情況以下圖所示:

複製算法.png

  1. 標記完以後,全部可達對象都被按次序複製到內存 B 中,並設置 B 爲當前使用中的內存。內存情況以下圖所示:

複製算法2.png

  • 優勢:按順序分配內存便可,實現簡單、運行高效,不用考慮內存碎片。
  • 缺點:可用的內存大小縮小爲原來的一半,對象存活率高時會頻繁進行復制。

標記-壓縮算法

須要先從根節點開始對全部可達對象作一次標記,以後,它並不簡單地清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。最後,清理邊界外全部的空間。所以標記壓縮也分兩步完成:日誌

  1. Mark 標記階段:找到內存中的全部 GC Root 對象,只要是和 GC Root 對象直接或者間接相連則標記爲灰色(也就是存活對象),不然標記爲黑色(也就是垃圾對象)。
  2. Compact 壓縮階段:將剩餘存活對象按順序壓縮到內存的某一端。

標記壓縮.png

  • 優勢:這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。
  • 缺點:所謂壓縮操做,仍須要進行局部對象移動,因此必定程度上仍是下降了效率

JVM分代回收策略

Java 虛擬機根據對象存活的週期不一樣,把堆內存劃分爲幾塊,通常分爲新生代、老年代,這就是 JVM 的內存分代策略。注意: 在 HotSpot 中除了新生代和老年代,還有永久代

分代回收的中心思想就是:對於新建立的對象會在新生代中分配內存,此區域的對象生命週期通常較短。若是通過屢次回收仍然存活下來,則將它們轉移到老年代中。

年輕代

新生成的對象優先存放在新生代中,新生代對象朝生夕死,存活率很低,在新生代中,常規應用進行一次垃圾收集通常能夠回收 70%~95% 的空間,回收效率很高。新生代中由於要進行一些複製操做,因此通常採用的 GC 回收算法是複製算法。

新生代又能夠繼續細分爲 3 部分:Eden、Survivor0(簡稱 S0)、Survivor1(簡稱S1)。這 3 部分按照 8:1:1 的比例來劃分新生代。這 3 塊區域的內存分配過程以下:

絕大多數剛剛被建立的對象會存放在 Eden 區。如圖所示:

新生代.png

當 Eden 區第一次滿的時候,會進行垃圾回收。首先將 Eden區的垃圾對象回收清除,並將存活的對象複製到 S0,此時 S1是空的。如圖所示:

新生代-S0.png 下一次 Eden 區滿時,再執行一次垃圾回收。這次會將 Eden和 S0區中全部垃圾對象清除,並將存活對象複製到 S1,此時 S0變爲空。如圖所示:

新生代s0-s1.png 如此反覆在 S0 和 S1之間切換幾回(默認 15 次)以後,若是還有存活對象。說明這些對象的生命週期較長,則將它們轉移到老年代中。如圖所示:

s0和s1來回切換15次.png

老年代

一個對象若是在新生代存活了足夠長的時間而沒有被清理掉,則會被複制到老年代。老年代的內存大小通常比新生代大,能存放更多的對象。若是對象比較大(好比長字符串或者大數組),而且新生代的剩餘空間不足,則這個大對象會直接被分配到老年代上。

咱們可使用 -XX:PretenureSizeThreshold 來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。老年代由於對象的生命週期較長,不須要過多的複製操做,因此通常採用標記壓縮的回收算法。

注意:對於老年代可能存在這麼一種狀況,老年代中的對象有時候會引用到新生代對象。這時若是要執行新生代 GC,則可能須要查詢整個老年代上可能存在引用新生代的狀況,這顯然是低效的。因此,老年代中維護了一個 512 byte 的 card table,全部老年代對象引用新生代對象的信息都記錄在這裏。每當新生代發生 GC 時,只須要檢查這個 card table 便可,大大提升了性能。

GC Log 分析

爲了讓上層應用開發人員更加方便的調試 Java 程序,JVM 提供了相應的 GC 日誌。在 GC 執行垃圾回收事件的過程當中,會有各類相應的 log 被打印出來。其中新生代和老年代所打印的日誌是有區別的。

  • 新生代 GC:這一區域的 GC 叫做 Minor GC。由於 Java 對象大多都具有朝生夕滅的特性,因此 Minor GC 很是頻繁,通常回收速度也比較快。
  • 老年代 GC:發生在這一區域的 GC 也叫做 Major GC 或者 Full GC。當出現了 Major GC,常常會伴隨至少一次的 Minor GC。

注意:在有些虛擬機實現中,Major GC 和 Full GC 仍是有一些區別的。Major GC 只是表明回收老年代的內存,而 Full GC 則表明回收整個堆中的內存,也就是新生代 + 老年代。

總結:

虛擬機垃圾回收機制不少時候都是影響系統性能、併發能力的主要因素之一。尤爲是對於從事 Android 開發的工程師來講,有時候垃圾回收會很大程度上影響 UI 線程,並形成界面卡頓現象。所以理解垃圾回收機制並學會分析 GC Log 也是一項必不可少的技能。

相關文章
相關標籤/搜索