從引用聊一聊 Java 垃圾回收

對象是 Java 世界的一等公民,所有的對象都是引用類型。除了 8 種基本的數據類型之外,其他的都是對象。

JVM 和垃圾回收

在開始討論引用之前,首先需要了解一下 JVM 和垃圾回收。Java 與 C 語言等不同,C 語言代碼經過編譯器編譯之後,就可以直接在 CPU 上執行,而 Java 不同,Java 語言需要先編譯生成字節碼文件,再由 JVM 生成可以在 CPU 上運行的代碼。

而且 Java 在生成對象時,並不需要手動分配內存,而是由 JVM 自動進行分配,對於不再使用的對象,JVM 會對這些對象佔用的內存進行回收,這個過程稱之爲垃圾回收(Garbage Collection,簡稱 GC)。

JVM 負責對程序運行時的內存進行管理。爲了提升管理效率,JVM 將運行時的內存劃分成了不同的區域,總體上的劃分如下:

每個線程獨佔一個虛擬機棧,通過程序計數器記錄當前代碼執行的位置,本地方法棧與虛擬機棧類似。

程序中創建的大多數對象都在堆中分配內存,然後棧中的變量通過引用來指向堆中的對象,所以堆是各個線程共享的一塊內存區域。方法區中則存儲 Java 的類型信息、常量、靜態變量等等(常量,靜態變量也有可能會引用對象)。

棧內的變量通過引用來和堆內的對象建立聯繫,建立聯繫的方式有兩種:使用句柄或者直接指針。

使用句柄方式如下:

使用直接指針如下:

使用句柄的好處是引用中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的數據,而不會修改引用中的數據。但是直接指針的速度會更快,在主流的 HotSpot 虛擬機中,使用的就是直接指針。

Java 引用

Java 中的引用類型總共有四種:強引用,軟引用,弱引用,虛引用。

強引用就是最普通的對象引用,每當 new 一個對象的時候,都是生成一個強引用。這種引用對垃圾回收免疫,即使發生 OOM,這種對象也不會被回收。

Object o = new Object();
複製代碼

軟引用的強度相對弱一些,在發生 OOM 之前,JVM 會嘗試去回收這些對象,軟引用的實現類是 SoftReference。

Object o = new Object();
SoftReference srf = new SoftReference(o);
複製代碼

弱引用就更弱了,如果碰上了垃圾回收,弱引用的對象肯定會被回收,弱引用的實現類是 WeakReference。

Object o = new Object();
WeakReference wrf = new WeakReference(o);
複製代碼

虛引用無法引用對象,實際只是做一些垃圾清理之後的事情, 虛引用的實現類是 PhantomReference。

Object o = new Object();
ReferenceQueue rq = new ReferenceQueue();
PhantomReference prf = new PhantomReference(o, rq);
複製代碼

上面的各種引用都繼承了 Reference 類,Reference 類中有一個 get 方法,如果軟引用和弱引用所指向的對象沒有被回收,那麼使用 get 方法就可以獲取原對象的引用。

Object o = new Object();
SoftReference srf = new SoftReference(o);
o = null; // 斷開強引用
System.out.println(srf.get()); // java.lang.Object@17579e0f
複製代碼

對軟引用手動觸發垃圾回收:

Object o = new Object();
SoftReference srf = new SoftReference(o);
o = null;
System.gc(); // 手動觸發垃圾回收
System.out.println(srf.get()); // java.lang.Object@17579e0f
複製代碼

由於內存充足,所以軟引用指向的對象並沒有被回收。對於弱引用來說,情況就不一樣:

Object o = new Object();
WeakReference wrf = new WeakReference(o);
o = null; // 斷開強引用
System.out.println(wrf.get()); // java.lang.Object@17579e0f
複製代碼

對弱引用手動觸發垃圾回收:

Object o = new Object();
WeakReference wrf = new WeakReference(o);
o = null;
System.gc(); // 手動觸發垃圾回收
System.out.println(wrf.get()); // null
複製代碼

由上面的代碼可知,弱引用一定會被垃圾回收。軟引用和弱引用一個經典的應用場景就是作爲緩存使用,這兩種引用所指向的對象一定會在發生 OOM 之前被回收,所以不會導致內存泄露問題。

虛引用 PhantomReference 的 get 方法會一直返回 null,所以無法通過虛引用獲取到對象。虛引用的意義在於提供了一種在對象被回收之後做某些事情的機制,在這裏就需要談到引用隊列。

ReferenceQueue 稱之爲引用隊列。如果我們爲一個引用指定一個引用隊列,那麼這個引用所指向的隊列在被垃圾回收後,該引用就會被加入到引用隊列中。

我們就可以根據引用隊列中的引用來判斷某個對象是否被回收,或者直接清除引用隊列的引用對象,具體的邏輯要看具體的業務場景。

引用和對象的可達性

假設新生成了一個對象:

Object o = new Object();
複製代碼

這個時候 o 是一個強引用,所以這個對象無法被回收。

o = null;
複製代碼

這樣一來,這個變量就不再指向這個對象了,假設也沒有其他類型的引用來指向這個對象,那麼這個對象就稱之爲不可達,就可以被回收了。

Java 中使用可達性分析來判斷對象是否要被回收。可達性的出發點是一些被稱之爲 GC Roots 的根對象,以下的對象可以作爲 GC Roots:

  • 棧中引用的對象
  • 方法區中靜態屬性引用的對象
  • 方法區中常量引用的對象
  • JVM 內部的引用,比如基本數據類型對應的 Class 對象

判斷一個對象是否存活其實就是通過引用的類型來進行判斷,對於弱引用和虛引用來說,基本就可以認爲是不可達了,在下次垃圾回收時就會被回收,而對於強引用,毫無疑問,肯定是可達的。

最難處理的就是軟引用。軟引用在 JVM 中並沒有明確把軟引用判斷爲可達還是不可達,而是會根據當前系統的狀態進行判斷,如果當前系統內存充足,那麼該對象就會被判斷爲可達,如果系統內存不足,那麼該對象就會傾向被回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章