Java 中的垃圾回收,常常是由 JVM 幫我們做好的。雖然這節省了大家很多的學習的成本,提高了項目的執行效率,但是當項目變得越來越複雜,用戶量越來越大時,還是需要我們懂得垃圾回收機制,這樣也能進行更深一步的優化。

<!-- more -->

辨別對象存亡

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

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

引用計數法

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

其具體實現爲:

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

如果一個指向某一對象的引用,被賦值爲其他值,那麼將該對象的引用計數器 -1。

也就是說,我們需要截獲所有的引用更新操作,並且相應地增減目標對象的引用計數器。

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

  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 的堆棧不會發生變化。

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

總結

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

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

https://death00.github.io/

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章