天天看點

JVM-垃圾收集算法

Java并不是最早使用自動記憶體管理的,早在Java之前的Lisp就已經開始使用了,該語言是1958年誕生于麻省理工的一門古老的進階程式設計語言。當時Lisp語言的設計者就想過一下問題:

  1. 什麼樣的對象需要回收
  2. 怎樣回收?
  3. 何時回收?

什麼樣的對象需要回收?

這個問題解釋起來很簡單相信任何一個人都可以回答:不再使用的對象都可以進行回收。可是應該如何定位這些對象呢?

  1. 引用計數算法
  2. 可達性分析算法

引用計數算法實作簡單,就是在對象上都設計一個計數器,每當有一個地方引用,就在該計數器上加一,反之就減一,當對象上的計數器值為零的時候那麼就代表該對象不再使用,即可以回收了。這種方式實作簡單,效率也相當不錯,但是有一個緻命問題:無法回收循環依賴的對象。

JVM-垃圾收集算法

 如果沒有任何地方引用A、B、C這三個對象時,但是這三個對象互相之間引用那麼他們的計數器的值最小也會是1,那麼這樣的對象就沒有辦法進行回收。當然也不是完全沒有辦法,隻是解決起來就比較費事了。Python使用的就是引用計數算法進行垃圾回收的。

至于可達性分析算法,就是預先定義系列GC Roots,如果對象到GC Roots之間不可達,那麼就證明該對象不在使用可以進行回收了。他的先天優勢就是可以處理循環依賴,但是實作起來就比較複雜了。

JVM-垃圾收集算法

 如圖所示,對象A、B、C之間互相引用,但是到GC Roots是不可達的那麼這三個對象就可以被回收了。GC Roots都有什麼呢?

  1. 虛拟機棧中引用的對象
  2. 方法區中類靜态屬性引用的對象
  3. 方法區中引用的常量對象
  4. 在本地方法棧中JNI引用的對象
  5. Java虛拟機的内部引用,例如基本資料類型對應的Class對象
  6. 所有被synchronized持有的對象
  7. 反映了Java虛拟機内部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等

怎樣回收

這裡就要談一下垃圾收集算法了:

  1. 标記-清除算法:該算法是最基礎、最簡單也是速度最快的垃圾收集算法,但是它會造成記憶體碎片化問題,以至于後邊建立新的對象時有可能無法找到足夠大的連續的記憶體空間而提前GC
  2. 标記-複制算法:該算法的宗旨是将一塊記憶體區域等分為兩塊大小相同的記憶體區域,每次隻使用其中的一塊,每當GC的時候就把目前使用的一半記憶體中存活的對象複制到另一半沒有使用的記憶體區域中,這樣做就解決了記憶體碎片化的問題,但是也同時浪費了一半的記憶體空間。
  3. 标記-整理算法:也叫标記壓縮算法,它的原理就是将所有存活的對象向記憶體區域的一端移動,清理剩下的記憶體空間。他與标記-清除算法的本質差別的就是後者是非移動式的而它是移動式的。他們兩個各有優點,标記-清除算法速度快但是會造成記憶體碎片化問題也就是配置設定記憶體時難,标記-整理算法不會産生記憶體碎片化問題,速度慢,該算法經常用到老年代中。

每一款垃圾收集器都使用了不同的垃圾收集算法。現在知道了怎麼區收集不用的對象,那麼如何定位呢?前邊提到過定位問題一般有兩種解決方案,引用計數算法和可達性分析算法,而在Java中使用的便是可達性分析算法。從垃圾收集算法中可以看到,所有算法都有一個标記的字眼,其實不難了解收集收集齊整體的收集思路就是先标記出那些對象是要被清除的,然後再進行統一清理。其中的标記過程就是要提到關于可達性分析算法的根節點枚舉了

根節點枚舉

根節點枚舉就是從GC Roots集合開始向下周遊,标記出存活的對象(也可以标記出死亡的對象)。但問題是随着Java應用的越發壯大,一個方法區都有可能有上幾百兆的,而且有一個緻命的問題就是根節點枚舉是STW(Stop The World: 停止所有使用者線程)的,那就有可能造成相當長的一段停頓時間。這可不是一個好主意,試想一下你在用着程式好好的突然來了幾十秒的,甚至幾分鐘的服務未響應是個什麼感覺?好在JVM有自己的解決辦法:根節點枚舉并不需要枚舉所有的GC Roots,而是在類加載完成的時候,Hotspot會把對象内什麼位置上是什麼類型的資料計算出來(即便是即時編譯也會有響應的記錄),存放到一組叫做OopMap的資料結構上。這樣收集器掃描的時候就能直接得知這些資訊了。

安全點

前邊說根節點枚舉的時候提到過STW,這是需要停止使用者線程的。那麼什麼時候停止?這就要談一下安全點了。

前邊提到過使用OopMap可以根節點枚舉的時候不去掃描所有的GC Roots,那麼這個OopMap是存放到哪裡呢?他是被設計在指令上的。但是也沒有必要為每一個指令都定義一個OopMap,而HotSpot實作上也是在特定的位置記錄這些資訊,這些位置被稱為安全點。有了安全點那麼就是說使用者線程不能在任意位置上停止,隻能到達安全點的時候才能夠停止。那麼如何保證所有線程在發生GC時都跑到安全點上呢?有兩種方案:

  1. 搶占式中斷:在發生GC虛拟機先将所有使用者線程停止,然後沒有到達安全點的線程繼續運作到最近的安全點上然後再次停止
  2. 主動式中斷:發生GC的時候設定一個标志位,所有線程在運作的時候不停的輪詢這個标志位如果為真則到最近的安全點上挂起線程

現在幾乎沒有虛拟機選用搶占式中斷了,幾乎所有的虛拟機都是采用主動式中斷。

常見的安全點如方法調用、循環跳轉、異常跳轉等一系列“長時間”執行的指令

安全區域

安全點幾乎可以解決使用者線程如何停頓了,但是如果有一個線程在發生GC的時候不執行,如調用了sleep?這個時候就需要安全區域的概念了,安全區域就是指能夠保證在某一段代碼片段之中,引用關系不會發生變化,是以在這個區域沒任意地方開始垃圾收集都是安全的。當線程進入安全區域的時候首先要辨別自己已經進入了安全區域,這段時間内發生垃圾收集就不必去管他們了。當線程要離開安全區域的時候,他們要檢查虛拟機是否完成了根節點枚舉,如果完成了繼續執行,否則直到收到可以離開安全區域的信号為止。

何時回收

這個問題很好回答,當建立新對象的時候沒有連續的記憶體空間容納新對象就需要進行垃圾回收。