JVM記憶體回收機制——哪些記憶體需要被回收(JVM學習系列2)

振宇要低調發表於2018-08-02

  上一篇文章中討論了Java記憶體執行時的各個區域,其中程式計數器、虛擬機器棧、本地方法棧隨執行緒生滅,且建立時需要多少記憶體,基本上在譯期間就決定的了,所以在記憶體回收時無需特殊的關注。而堆和方法區則不同,首先堆中只能在執行時,隨著方法的呼叫而確定建立哪些物件;方法區中也同樣如此,常量池中的常量、載入的類資訊也是隨時在發生著變化且不可預知。所以說,JVM記憶體回收,主要針對的是這兩部分的內容。

1、堆中“死”物件

  籠統的說,沒用的物件就是死物件。

1.1如何判定物件“已死”

1.1.1引用計數法

  給物件新增一個引用計數器,當有其他物件引用該物件時,計數器+1;當引用失效時,計數器-1。原理簡單,實現容易,聽起來也不錯。但這個演算法無法解決物件間迴圈引用的問題。也就是說物件A引用了物件B,而物件B同時也引用了物件A,此時就形成了迴圈引用。這樣兩個物件就永遠都不會被回收。主流的JVM中均沒有采用這種演算法。

1.1.2可達性分析

  基本思路是找一些物件作為遍歷的起始點(成為GC Roots),從這些起始點開始搜尋,當某個物件並沒有和任何GC Root產生關聯,則認為這個物件已經不被使用了,可以清除。通常,可作為GC Root的物件有以下幾種:

    1)虛擬機器棧,棧幀中的本地變數表中引用的物件

    2)方法區中載入的類的靜態屬性引用的物件

    3)方法區中常量引用的物件(疑問,方法區中的常量到底有哪些)

    4)本地方法棧中引用的物件

  由上可見,可作為GC Roots的物件基本上可以歸類為全域性性引用和執行上下文,而應用中這些物件實在太多,這就導致根節點的遍歷(或者叫確定GC Roots)勢必會消耗很多時間,那是如何確定GC Roots的呢?當前主流Java虛擬機器使用的都是準確式GC,以HotSpot虛擬機器為例,當系統確定GC Roots時,並不需要遍歷整個方法區或者堆,而是知道哪些地方存放著物件的引用,這是有一個叫OopMap的資料結構來實現的。

1.2關於引用

  上文談到的引用,是我們平時經常談到的、最直白的“引用”。在JDK1.2之前,“引用”的定義是:如果reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。這種定義非常的純粹,非黑即白。在記憶體回收的時候也是,有用就留著,沒用立刻刪。但有一些場景,例如,當記憶體充足的時候,某塊記憶體留著備用;記憶體不充足的時候,回收這塊記憶體。這時純粹的“引用”就沒辦法了。在JDK1.2之後,Java對引用進行了擴充套件,將引用分為強引用、軟引用、弱引用、虛引用。

  1)強引用:就是上面說的純粹的引用

String str=new String("abc"); 

  2)軟引用:表示物件還有用,但不是必須的。記憶體不夠用的時候,才會回收這些記憶體。軟引用可用來實現記憶體敏感的快取記憶體。

String str=new String("abc");                                     // 強引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 軟引用 

後續在垃圾回收時,JVM內部邏輯如下:

if(JVM.記憶體不足()) {
    str = null;  // 轉換為軟引用
    System.gc(); // 垃圾回收器進行回收
}

  3)弱引用:表示物件還有用,但不是必須的,且不如軟引用“硬”。只要發生垃圾回收,這些物件就會被回收。

String str=new String("abc");    
WeakReference<String> weakRef = new WeakReference<String>(str);

後續在垃圾回收時,JVM內部邏輯如下:

str = null;

  4)虛引用:最弱的引用,一個物件是否存在虛引用,並不影響其生存。為一個物件設定虛引用的唯一目的是在該物件被回收時,能夠收到一個系統通知。 虛引用主要用來跟蹤物件被垃圾回收器回收的活動。

2、方法區內記憶體

  JDK1.8之前方法區是放到永久代中實現的(HotSpot虛擬機器)。對於堆中的記憶體回收,尤其是新生代,回收率能夠達到70%~95%;而永久代中的回收率則非常低。也就是說,回收方法區內的記憶體價效比很低。但這塊記憶體又不能沒有垃圾回收機制,SUN公司就曾公佈過關於方法區記憶體洩漏的嚴重BUG。

  方法區中垃圾回收主要關注兩部分:無用的常量和類。

  常量的回收與堆中物件的回收機制類似,當某個常量沒有被任何物件引用的時候,這個常量就沒有用了,就可以被回收。舉例,當前系統中找不到任何String物件引用了常量池中的的某個字串常量:abcd,那麼abcd這個常量就會被回收。同理,常量池中其他類、方法、欄位的符號引用也與此類似。

  回收類的效率非常低,但在當前企業級應用大量使用反射(Spring IOC,在bean注入的時候,通過反射例項化一個類,將其通過setter方法放到bean中)、動態代理(Spring AOP,AOP框架不會去修改位元組碼,而是在記憶體中臨時為方法生成一個AOP物件,這個AOP物件包含了目標物件的全部方法,並且在特定的切點做了增強處理,並回撥原物件的方法。)、CGLib(CGLib技術後續繼續學習)等技術的前提下,類的回收也變得很重要。已載入類的回收條件非常苛刻,需要滿足以下三個條件,才有可能被JVM回收:

    1)該類產生的物件例項均被回收

    2)載入該類的ClassLoader已經被回收(類載入機制後續繼續學習)

    3)該類的類物件沒有在任何地方引用,無法反射出這個類的方法

相關文章