JVM垃圾回收(上)

Java 中的垃圾回收,經常是由 JVM 幫咱們作好的。雖然這節省了你們不少的學習的成本,提升了項目的執行效率,可是當項目變得愈來愈複雜,用戶量愈來愈大時,仍是須要咱們懂得垃圾回收機制,這樣也能進行更深一步的優化。 git

辨別對象存亡

垃圾回收( Garbage Collection,如下簡稱 GC ),從字面上理解,就是將已經分配出去的,但卻再也不使用的內存回收回來,以便可以再次分配。github

在 JVM 中,垃圾就是指的死亡對象所佔據的堆空間( GC 是發生在堆空間中),那麼咱們若是辨別一個對象是否死亡呢?JVM 使用的是引用計數法可達性分析安全

引用計數法

引用計數法( Reference Counting),是爲每一個對象添加一個引用計數器,用來統計引用該對象的個數。一旦某個對象的引用計數器爲0,則說明該對象已經死亡,即可以被回收了。多線程

其具體實現爲:學習

若是有一個引用,被賦值爲某一對象,那麼將該對象的引用計數器 +1。優化

若是一個指向某一對象的引用,被賦值爲其餘值,那麼將該對象的引用計數器 -1。線程

也就是說,咱們須要截獲全部的引用更新操做,而且相應地增減目標對象的引用計數器。code

看似很簡單的實現,其實裏面有很多缺陷:cdn

  1. 須要額外的空間來存儲計數器。
  2. 計數器的更新操做十分繁瑣。
  3. 最重要的:沒法處理循環引用對象。

針對第3點,舉個例子特別說明一下:對象

假設對象 a 與 b 相互引用,除此以外沒有其餘引用指向他們。在這種狀況下,a 和 b 實際上已經死了。

但因爲它們的引用計數器皆不爲0(由於相互引用,二者均爲1),在引用計數法的計算中,這兩個對象還活着。所以,這些循環引用對象所佔據的空間將不可回收,從而形成了內存泄露

可達性分析

可達性分析( Reachability Analysis ),是目前 JVM 主要採起的斷定對象死亡的方法。實質在於將一系列GC Roots做爲初始的存活對象合集(live set),而後從該合集出發,探索全部可以被該集合引用到的對象,並將其加入到該集合中,這個過程咱們也稱之爲標記(mark)。最終,未被探索到的對象即是死亡的,是能夠回收的。

那麼什麼是GC Roots呢?咱們能夠暫時理解爲由堆外指向堆內的引用,通常而言,GC Roots 包括(但不限於)以下幾種:

  1. Java 方法棧楨中的局部變量
  2. 已加載類的靜態變量
  3. JNI handles
  4. 已啓動且未中止的 Java 線程

以前咱們說引用計數法會有循環引用的問題,可達性分析就不會了。舉例來講,即使對象 a 和 b 相互引用,只要從 GC Roots 出發沒法到達 a 或者 b,那麼可達性分析便會認爲它們已經死亡。

可達性分析有沒有什麼缺點呢?有的,在多線程環境下,其餘線程可能會更新已經分析過的對象中的引用,從而形成誤報(將引用設置爲 null)或者漏報(將引用設置爲未被訪問過的對象)。

誤報並無什麼傷害,JVM 至多損失了部分垃圾回收的機會。漏報則比較麻煩,由於垃圾回收器可能回收事實上仍被引用的對象內存。一旦從原引用訪問已經被回收了的對象,則頗有可能會直接致使 JVM 崩潰。

STW

既然可達性分析在多線程下有缺點,那 JVM 是如何解決的呢?答案即是 Stop-the-world(如下簡稱JWT),中止了其餘非垃圾回收線程的工做直到完成垃圾回收。這也就形成了垃圾回收所謂的暫停時間(GC pause)。

那 SWT 是如何實現的呢?當 JVM 收到 SWT 請求後,它會等待全部的線程都到達安全點(Safe Point),才容許請求 SWT 的線程進行獨佔的工做。

那什麼又叫安全點呢?安全點是 JVM 能找到一個穩定的執行狀態,在這個執行狀態下,JVM 的堆棧不會發生變化。

這麼一來,垃圾回收器便可以「安全」地執行可達性分析,全部存活的對象也均可以成功被標記,那麼以後就能夠將死亡的對象進行垃圾回收了。

總結

以上即是發現死亡對象的過程,這也爲以後的垃圾回收進行鋪墊,具體的垃圾回收過程,我會在下一篇文章中講述,敬請期待。

有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。

death00.github.io/

相關文章
相關標籤/搜索