淺談JVM垃圾回收之哪些對象可以被回收
1.背景
Java語言相比於C和C++,一個最大的特點就是不需要程序員自己手動去申請和釋放內存,這一切交由JVM來完成。在Java中,運行時的數據區域分為程序計數器、Java虛擬機棧、本地方法棧、方法區和堆。其中,程序計數器、虛擬機棧和本地方法棧是線程私有的,線程銷毀後自動釋放。垃圾回收的行為發生在堆和方法區,主要是堆,而堆中存儲的主要是對象。那麼自然而然地就會有這麼幾個問題,哪些對象可以被回收?通過什麼方式回收?本文主要探討第一個問題,以及JVM對Java中幾種引用的回收策略。
2.如何判斷一個對象是否可以被回收
2.1 引用計數法
主要思想是:給對象添加一個引用計數器,這個對象被引用一次,計數器就加1;不再引用瞭,計數器就減1。如果一個對象的引用計數器為0,說明沒有人使用這個對象,那麼這個對象就可以被回收瞭。這種方法實現起來比較簡單,效率也比較高,大多數情況下都是有效的。但是,這種方法有一個漏洞。比如A.property = B,B.property = A,A和B兩個對象互相引用,並且沒有其他對象引用A和B。按照引用計數法的思想,A和B對象的引用計數器都不為0,都不能被釋放,但實際情況是A和B已經沒人使用他們瞭,這就造成瞭內存泄漏。所以,引用計數法雖然實現簡單,但並不是一個完美的解決方案,實際中的Java也沒有采用它。
2.2 可達性分析算法
主要思想是:首先確定確定一系列肯定不能被回收的對象,即GC Roots。然後,從這些GC Roots出發,向下搜索,去尋找它直接和間接引用的對象。最後,如果一個對象沒有被GC Roots直接或間接地引用,那麼這個對象就可以被回收瞭。這種方法可以有效解決循環引用的問題,實際中Java也是采用這種判斷方法。那麼問題來瞭,哪些對象可以作為GC Roots呢?這裡可以使用MAT工具進行觀察。運行下面的demo:
import java.util.concurrent.TimeUnit; public class GCRootsTest { public static void main(String[] args) throws InterruptedException { Object o = new Object(); TimeUnit.SECONDS.sleep(100); } }
主線程sleep的時候,在terminal窗口執行jmap -dump:format=b,live,file=heapdump.bin 2872命令,生成堆轉儲快照dump文件,其中2872是進程id,可以使用jps命令查看。然後使用MAT工具打開dump文件,可以很明顯地看到一共有四類對象可以作為GC Roots,下面詳細介紹下。
第一類,系統類對象(System Class)。比如,java.lang.String的Class對象,這個也很好理解,如果這些核心的系統類對象被回收瞭,程序就沒辦法運行瞭。
第二類,native方法引用的對象。
第三類,活動線程中正在引用的對象。可以看出,代碼中變量o指向的Object對象可以被當作GC Roots。
第四類,正在加鎖的對象。
3.Java中的幾種引用
在可達性分析算法中,判斷一個對象是不是可以被回收,主要看從GC Roots出發是否可以找到一個引用指向該對象。java中的引用一共有四種,按照引用的強弱依次為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)。這樣就可以對不同引用指向的對象采取不同的回收策略。比如一個強引用指向一個對象,那麼這個對象肯定不會被回收,哪怕發生OOM。而對於弱引用指向的對象,隻要發生垃圾回收,該對象就會被回收。下面詳細介紹下不同引用的用法。
3.1強引用
所謂強引用,就是平時使用最多的,類似於Object obj = new Object()的引用。垃圾回收器永遠不會回收被強引用指向的對象。
3.2軟引用
軟引用,在Java中使用SoftReference類來實現軟引用。在下面的代碼中,softReference作為軟用指向一個Object對象,而otherObject變量可以通過軟引用的get方法間接引用到Object對象。
public static void main(String[] args) { // 軟引用 SoftReference<Object> softReference = new SoftReference<>(new Object()); Object otherObject = softReference.get(); }
對於軟引用指向的對象,當內存不夠用時,該對象就會被回收。為演示這個現象,將JVM的堆內存設置為10M(-Xms10M -Xmx10M)。以下代碼的主要邏輯是:向一個List集合中添加5個SoftReference對象,其中每個SoftReference對象都指向瞭一個大小為2M的byte數組,添加完成之後遍歷List,並打印List中每一個軟引用指向的對象。
public class ReferenceTest { private static final int _2M = 2 * 1024 * 1024; public static void main(String[] args) { List<SoftReference<Object>> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { SoftReference<Object> softReference = new SoftReference<>(new byte[_2M]); list.add(softReference); } System.out.println("List集合中的軟引用:"); for (int i = 0; i < 5; i++) { System.out.println(list.get(i)); } System.out.println("--------------------------"); System.out.println("List集合中的軟引用指向的對象:"); for (int i = 0; i < 5; i++) { System.out.println(list.get(i).get()); } } }
上述代碼在堆內存為10M的情況下運行的結果如下圖。可以看到前三個軟引用指向的對象已經被垃圾回收器回收掉瞭,原因就是堆內存不夠用瞭,軟引用指向的對象就被回收瞭。
通常情況下,軟引用指向的對象被回收瞭,那麼這個軟引用也就沒有存在的意義瞭,應該被垃圾回收器回收掉。為瞭實現這個效果,通常軟引用要配合引用隊列使用。用法如下面的代碼所示,將軟引用和引用隊列關聯,這樣當軟引用指向的對象被回收時,該軟引用會自動加入到引用隊列,這時候可以采用一定的策略將這些軟引用對象回收。
public class ReferenceTest { private static final int _2M = 2 * 1024 * 1024; public static void main(String[] args) { List<SoftReference<Object>> list = new ArrayList<>(); // 引用隊列 ReferenceQueue<Object> queue = new ReferenceQueue<>(); for (int i = 0; i < 5; i++) { // 同時將軟引用關聯引用隊列,當軟引用指向的對象被回收時,該軟引用會加入到隊列 SoftReference<Object> softReference = new SoftReference<>(new byte[_2M], queue); list.add(softReference); } // 移除List中,指向對象已經被回收的軟引用 Reference<?> poll = queue.poll(); while (null != poll) { list.remove(poll); poll = queue.poll(); } System.out.println("List集合中的軟引用:"); for (SoftReference<Object> reference : list) { System.out.println(reference); } System.out.println("-------------------------------------"); System.out.println("List集合中的軟引用指向的對象:"); for (SoftReference<Object> reference : list) { System.out.println(reference.get()); } } }
執行結果如下:
3.3弱引用
弱引用,相比於軟引用,它的引用程度更弱。隻要發生垃圾回收,弱引用指向的對象都會被回收。話不多說,直接上代碼。跟軟引用的demo差不多,唯一不同的是每個byte的數組的大小變成瞭2K,這樣堆肯定放的下,也不會發生垃圾回收。
public class WeakReferenceTest { private static final int _2K = 2 * 1024; public static void main(String[] args) { List<WeakReference<byte[]>> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { WeakReference<byte[]> reference = new WeakReference<>(new byte[_2K]); list.add(reference); } System.out.println("List集合中的軟引用:"); for (WeakReference<byte[]> reference : list) { System.out.println(reference); } System.out.println("-------------------------------------"); System.out.println("List集合中的軟引用指向的對象:"); for (WeakReference<byte[]> reference: list) { System.out.println(reference.get()); } } }
運行。可以看到弱引用指向的對象並沒有被回收。
在上述代碼的基礎上,人為的進行一次垃圾回收,代碼如下。
public class WeakReferenceTest { private static final int _2K = 2 * 1024; public static void main(String[] args) { List<WeakReference<byte[]>> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { WeakReference<byte[]> reference = new WeakReference<>(new byte[_2K]); list.add(reference); } System.gc(); // 手動垃圾回收 System.out.println("List集合中的弱引用:"); for (WeakReference<byte[]> reference : list) { System.out.println(reference); } System.out.println("-------------------------------------"); System.out.println("List集合中的弱引用指向的對象:"); for (WeakReference<byte[]> reference: list) { System.out.println(reference.get()); } } }
運行。發現此時弱引用指向的對象都被回收掉瞭。和軟引用一樣,弱引用也可以結合引用隊列使用,這裡不再贅述。
3.4虛引用
與軟引用和虛引用不同,虛引用必須配合引用隊列使用,而且不能通過虛引用獲取到虛引用指向的對象。在Java中虛引用使用PhantomReference類來表示,從PhantomReference的源碼可以看出調用虛引用的get方法始終返回的是null,而且PhantomReference隻提供瞭包含引用隊列的有參構造器,這也就是說虛引用必須結合引用隊列使用。
public class PhantomReference<T> extends Reference<T> { public T get() { return null; } public PhantomReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
既然不能通過虛引用獲取到它指向的對象,那麼虛引用到底有什麼用呢?實際上,為一個對象關聯虛引用的唯一目的就是:在該對象被垃圾回收時收到一個系統通知。當垃圾回收器準備回收一個對象時,如果發現還有虛引用與之關聯,就會在垃圾回收後,將這個虛引用加入引用隊列,在其關聯的虛引用出隊前,不會徹底銷毀該對象。 上面的描述還是不夠通俗易懂,其實虛引用的一個經典的使用場景就是和DirectByteBuffer類關聯使用。DirectByteBuffer類使用的是堆外內存(服務器內存中,除瞭JVM占用外的那部分),省去瞭數據到內核的拷貝,因此效率比ByteBuffer要高很多(這裡的重點是虛引用,想要瞭解DirectByteBuffer類的底層原理,可以在網上找下資源),它的內存示意圖如下。
雖然DirectByteBuffer類的效率很高,但是由於堆外內存JVM的垃圾回收器不能進行回收,所以要謹慎處理DirectByteBuffer類使用的堆外內存,否則極易造成服務器內存泄漏。為瞭解決這個問題,虛引用就派上用場瞭。DirectByteBuffer類的創建和回收主要分為以下幾個步驟
創建DirecByteBuffer對象時會同時創建一個Cleaner虛引用對象,指向自己,同時傳一個Deallocator對象給Cleaner
Cleaner類的父類是PhantomReference,爺爺類是Reference。Reference類在初始化的時候會啟動一個ReferenceHandler線程
當DirectByteBuffer對象被回收後,Cleaner對象會被加入引用隊列
這時ReferenceHandler線程會調用Cleaner對象的clean方法完成對堆外內存的回收
clean方法會調用Deallocator的run方法,通過Unsafe類最終完成堆外內存的回收
總結起來就是一句話,用虛引用關聯DirectByteBuffer對象,當DirectByteBuffer被回收後,虛引用對象會被加入到引用隊列,進而由該虛引用對象完成對堆外內存的釋放。(感興趣的或夥伴可以跟以下DirectByteBuffer的源碼)
4.總結
- JVM采用可達性分析算法來判斷堆中有哪些對象可以被回收。
- 主要有四類對象可作為GC Roots:系統類對象、Native方法引用的對象、活動線程引用的對象以及正在加鎖的對象。
- Java中常用的引用主要有四種,強引用、軟引用、弱引用和虛引用,對不同引用指向的對象,JVM有不同的回收策略。
- 對於強引用指向的對象,垃圾回收器不會將其回收,即使是發生OOM。
- 對於軟引用指向的對象,當內存不夠時,垃圾回收器會將其回收。這個特點可以用來實現緩存,當內存不足時JVM會自動清理掉這些緩存。
- 對於弱引用指向的對象,當發生垃圾回收時,垃圾回收器會將其回收。
- 對於虛引用,必須配合引用隊列使用,而且不能通過虛引用獲取到虛引用指向的對象,為一個對象關聯虛引用的唯一目的就是在該對象被垃圾回收時收到一個系統通知。
到此這篇關於JVM垃圾回收之哪些對象可以被回收的文章就介紹到這瞭,更多相關JVM垃圾回收之哪些對象可以被回收內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!