細說Java中的各種引用以及GC策略

在介紹各種引用之前,先簡單介紹下垃圾回收

什麼是垃圾回收

  • 垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可以使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。
  • Java 語言出來之前,大家都在拼命的寫 C 或者 C++ 的程序,而此時存在一個很大的矛盾,C++ 等語言創建對象要不斷的去開闢空間,不用的時候又需要不斷的去釋放控件,既要寫構造函數,又要寫析構函數,很多時候都在重複的 allocated,然後不停的析構。於是,有人就提出,能不能寫一段程序實現這塊功能,每次創建,釋放控件的時候複用這段代碼,而無需重複的書寫呢?
  • 1960年,基於 MIT 的 Lisp 首先提出了垃圾回收的概念,用於處理C語言等不停的析構操作,而這時 Java 還沒有出世呢!所以實際上 GC 並不是Java的專利,GC 的歷史遠遠大於 Java 的歷史!

Java中的垃圾回收是根據可達性分析算法來判斷對象是否存活的

可達性分析算法

在主流的商用程序語言(Java、C#,甚至包括前面提到的古老的Lisp)的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖3-1所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定爲是可回收的對象。
40a02a9c109456d7716224ccb078de95da414f01.jpeg

在Java語言中,可作爲GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

各種引用

對象是否存活與“引用”有關。

在JDK 1.2以前,Java中的引用的定義很傳統:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的對象就顯得無能爲力(這裏說的就是強引用,最基本的引用方式)。

我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。

在JDK 1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

引用類型 GC策略 簡介
強引用(Strong Reference) 永遠不會回收(GC ROOT可引用到的前提下) 最基本的引用Object obj=new Object()
軟引用(Soft Reference) OOM之前回收 SoftReference
弱引用(Weak Reference) 下一次GC前 WeakReference
虛引用(Phantom Reference) 未知,也就是隨時可能被回收 PhantomReference

強引用

強引用就是最基本的引用方式,Object obj=new Object(),引用的是另一塊內存的起始地址。強引用的對象回收基於“可達性分析”算法,當對象不可達時纔可能會被回收。

比如方法中new的對象,引用賦值給方法內的局部變量(局部變量存儲在棧幀中的局部變量表),當方法結束之後,棧幀出棧,對象就自然不可達了,不可達就可能會被回收

軟引用

軟引用是用來描述一些還有用但並非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。在JDK
1.2之後,提供了SoftReference類來實現軟引用。
SoftReference<RefObj> ref = new SoftReference<RefObj>(refObj);

寫個例子來測試下軟引用的GC策略:

# JVM OPTIONS: -XX:+PrintGCDetails -Xmx5m
public class ReferenceTest {

    private List<RefObj> refObjs = new ArrayList<>();

    private SoftReference<RefObj> ref = new SoftReference<RefObj>(createRefObj(4096*256));//1m

    public void add(){
        refObjs.add(createRefObj(4096));
    }

    private RefObj createRefObj(int dataSize){
        RefObj refObj = new RefObj();
        byte[] data = new byte[dataSize];
        for (int i = 0; i < dataSize; i++) {
            data[i] = Byte.MAX_VALUE;
        }
        refObj.setData(data);
        return refObj;
    }
    public void validRef(){
        System.out.println(ref.get());
    }

    public static void main(String[] args) {
        ReferenceTest referenceTest = new ReferenceTest();
        for (int i = 0; i < 1200; i++) {
        //不停新增堆大小
            referenceTest.add();
        //新增後查看SoftReference中的對象是否被回收
            referenceTest.validRef();
        }

    }

    private class RefObj{
        private byte[] data;

        public byte[] getData() {
            return data;
        }

        public void setData(byte[] data) {
            this.data = data;
        }
    }
}

ReferenceTest中維護一個RefObjList和一個SoftReference,往RefObjList不斷添加對象,增加堆大小,直至內存溢出。來觀察下SoftReference中引用的對象是否還存在

測試結果:

# 截取一段關鍵部分
[Full GC (Ergonomics) [PSYoungGen: 1023K->1021K(1536K)] [ParOldGen: 4073K->4073K(4096K)] 5097K->5094K(5632K), [Metaspace: 3534K->3534K(1056768K)], 0.0017581 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
reference.ReferenceTest$RefObj@58372a00
reference.ReferenceTest$RefObj@58372a00
reference.ReferenceTest$RefObj@58372a00
reference.ReferenceTest$RefObj@58372a00
reference.ReferenceTest$RefObj@58372a00
[Full GC (Ergonomics) [PSYoungGen: 1024K->1021K(1536K)] [ParOldGen: 4093K->4093K(4096K)] 5117K->5114K(5632K), [Metaspace: 3534K->3534K(1056768K)], 0.0014771 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 1021K->0K(1536K)] [ParOldGen: 4093K->4072K(4096K)] 5114K->4072K(5632K), [Metaspace: 3534K->3534K(1056768K)], 0.0060554 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
null
.....省略
[Full GC (Allocation Failure) [PSYoungGen: 1022K->1022K(1536K)] [ParOldGen: 4093K->4093K(4096K)] 5116K->5116K(5632K), [Metaspace: 3534K->3534K(1056768K)], 0.0014051 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "main" [Full GC (Ergonomics) [PSYoungGen: 1024K->0K(1536K)] [ParOldGen: 4094K->981K(4096K)] 5118K->981K(5632K), [Metaspace: 3538K->3538K(1056768K)], 0.0037282 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
    at reference.ReferenceTest.createRefObj(ReferenceTest.java:21)
    at reference.ReferenceTest.add(ReferenceTest.java:16)
    at reference.ReferenceTest.main(ReferenceTest.java:35)
Heap
 PSYoungGen      total 1536K, used 39K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 3% used [0x00000000ffe00000,0x00000000ffe09e10,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 4096K, used 981K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 23% used [0x00000000ffa00000,0x00000000ffaf55f0,0x00000000ffe00000)
 Metaspace       used 3565K, capacity 4564K, committed 4864K, reserved 1056768K
  class space    used 384K, capacity 388K, committed 512K, reserved 1048576K

從程序+GC日誌中可以看出,在某次GC後(OOM前的GC),SoftReference中引用的對象獲取不到了,已經被GC回收。

弱引用

弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK
1.2之後,提供了WeakReference類來實現弱引用。

弱引用和軟引用使用方式一致,只是對應的類不同,和GC策略不同而已。

WeakReference<RefObj> ref = new WeakReference<RefObj>(refObj);

基於上面軟引用的測試代碼稍作修改,來測試下弱引用的GC策略:

public class ReferenceTest {

    private List<RefObj> refObjs = new ArrayList<>();

    private WeakReference<RefObj> ref = new WeakReference<RefObj>(createRefObj(4096*256));//1m

    public void add(){
        refObjs.add(createRefObj(4096));
    }

    private RefObj createRefObj(int dataSize){
        RefObj refObj = new RefObj();
        byte[] data = new byte[dataSize];
        for (int i = 0; i < dataSize; i++) {
            data[i] = Byte.MAX_VALUE;
        }
        refObj.setData(data);
        return refObj;
    }
    public void validRef(){
        System.out.println(ref.get());
    }

    public static void main(String[] args) {
        ReferenceTest referenceTest = new ReferenceTest();
        referenceTest.validRef();
        referenceTest.add();
        //手動GC後查看WeakReference中的對象是否還存在
        System.gc();
        referenceTest.validRef();
    }

    private class RefObj{
        private byte[] data;

        public byte[] getData() {
            return data;
        }

        public void setData(byte[] data) {
            this.data = data;
        }
    }
}

測試結果:

[GC (Allocation Failure) [PSYoungGen: 1526K->512K(1536K)] 1992K->1266K(5632K), 0.0005932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
reference.ReferenceTest$RefObj@58372a00
[GC (System.gc()) [PSYoungGen: 706K->512K(1536K)] 2484K->2346K(5632K), 0.0005772 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 512K->0K(1536K)] [ParOldGen: 1834K->972K(4096K)] 2346K->972K(5632K), [Metaspace: 3493K->3493K(1056768K)], 0.0062458 secs] [Times: user=0.16 sys=0.00, real=0.01 secs] 
null
Heap
 PSYoungGen      total 1536K, used 31K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 3% used [0x00000000ffe00000,0x00000000ffe07cc8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 972K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 23% used [0x00000000ffa00000,0x00000000ffaf31d8,0x00000000ffe00000)
 Metaspace       used 3500K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

從日誌上看,在任何一次GC(包括手動GC)之後,GC會回收WeakReference中的對象,無論當前內存是否足夠。

虛引用

虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK
1.2之後,提供了PhantomReference類來實現虛引用。

也就是說,虛引用的對象,隨時都有可能被回收

虛引用的用法和軟引用/弱引用也很類似,只是在構造的時候,需要指定一個隊列

//引用隊列,當引用的對象被回收後,Reference對象本身會被添加到referenceQueue中,相當於得到了一個通知
//軟引用/弱引用中都有此構造參數,只是在虛引用中此參數變成必傳了而已
ReferenceQueue<RefObj> referenceQueue = new ReferenceQueue<>();

PhantomReference<RefObj> ref = new PhantomReference<RefObj>(refObj,referenceQueue);

SoftReference<RefObj> ref = new SoftReference<RefObj>(refObj,referenceQueue);

WeakReference<RefObj> ref = new WeakReference<RefObj>(refObj,referenceQueue);

還是基於上面的測試代碼稍作修改,來測試一下虛引用以及回收隊列

public class ReferenceTest {

    private List<RefObj> refObjs = new ArrayList<>();

    private ReferenceQueue<RefObj> referenceQueue = new ReferenceQueue<>();

    private PhantomReference<RefObj> ref = new PhantomReference<RefObj>(createRefObj(4096*256),referenceQueue);//1m

    public void add(){
        refObjs.add(createRefObj(4096));
    }

    /**
     * 啓一個子線程,監控回收的引用隊列
     */
    public void referenceQueueMonitor(){
        new Thread(()->{
            try{
                Reference<RefObj> weakReference;
                while ((weakReference = (Reference<RefObj>) referenceQueue.remove())!=null){
                    System.out.println("collect "+weakReference);
                }
            }catch (InterruptedException e){}
        }).start();
    }

    private RefObj createRefObj(int dataSize){
        RefObj refObj = new RefObj();
        byte[] data = new byte[dataSize];
        for (int i = 0; i < dataSize; i++) {
            data[i] = Byte.MAX_VALUE;
        }
        refObj.setData(data);
        return refObj;
    }
    public void validRef(){
        System.out.println(ref.get());
    }

    public static void main(String[] args) {
        ReferenceTest referenceTest = new ReferenceTest();
        referenceTest.referenceQueueMonitor();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        referenceTest.validRef();
        referenceTest.add();
        System.gc();
        referenceTest.validRef();
    }

    private class RefObj{
        private byte[] data;

        public byte[] getData() {
            return data;
        }

        public void setData(byte[] data) {
            this.data = data;
        }
    }
}

測試結果:

#第一次:
[GC (Allocation Failure) [PSYoungGen: 1525K->512K(1536K)] 3466K->2661K(5632K), 0.0005258 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
null
[GC (System.gc()) [PSYoungGen: 567K->512K(1536K)] 2717K->2717K(5632K), 0.0005225 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 512K->0K(1536K)] [ParOldGen: 2205K->2213K(4096K)] 2717K->2213K(5632K), [Metaspace: 4379K->4379K(1056768K)], 0.0078481 secs] [Times: user=0.13 sys=0.00, real=0.01 secs] 
null
#最後打印了回收日誌
collect java.lang.ref.PhantomReference@7be2d776

#第N次:
[GC (Allocation Failure) [PSYoungGen: 1536K->512K(1536K)] 3440K->2536K(5632K), 0.0005524 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#先打印了回收日誌
collect java.lang.ref.PhantomReference@30a3ced6
null
[GC (System.gc()) [PSYoungGen: 573K->512K(1536K)] 2597K->2580K(5632K), 0.0005956 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 512K->0K(1536K)] [ParOldGen: 2068K->2252K(4096K)] 2580K->2252K(5632K), [Metaspace: 4387K->4387K(1056768K)], 0.0082860 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
null

從日誌上,多次結果並不一致。由於程序啓動時會發生GC,中途又手動觸發了GC。結合虛引用的特點來看,虛引用的的對象存活週期並不能確定,對象可能在任何時候被回收。

各種引用的引用場景

我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。

最容易想到的就是緩存了,內存不足時釋放部分數據,類似Redis/Ehcache之類的淘汰策略。

下面列出一下JDK/框架中的應用場景:

  • java.util.WeakHashMap - jdk
  • java.util.concurrent.ArrayBlockingQueue - jdk
  • org.springframework.util.ConcurrentReferenceHashMap - spring中大量使用了此緩存,包括spring-BeanUtils

參考

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