1. 程式人生 > 實用技巧 >JVM強引用、軟引用、弱引用、虛引用、終結器引用垃圾回收特點總結

JVM強引用、軟引用、弱引用、虛引用、終結器引用垃圾回收特點總結

JVM引用

  • 我們希望能描述這樣一類物件: 當記憶體空間還足夠時,則能保留在記憶體中;如果記憶體空間在進行垃圾收集後還是很緊張,則可以拋棄這些物件。 -【既偏門又非常高頻的面試題】強引用、軟引用、弱引用、虛引用有什麼區別?具體使用.場景是什麼?
  • 在JDK 1.2版之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference) 、弱引用(Weak Reference) 和虛引用(Phantom Reference) 4種,這4種引用強度依次逐漸減弱。
  • 除強引用外,其他3種引用均可以在java.lang.ref包中找到它們的身影。如下圖,顯示了這3種引用型別對應的類,開發人員可以在應用程式中直接使用它們。

Reference子類中只有終結器引用是包內可見的(該類沒有被public修飾),其他3種引用型別均為public class,可以在應用程式中直接使用。

對於強、軟、弱、虛這四種引用物件的垃圾回收特點的描述,都是指的在引用關係還存在的情況下:

  • 強引用(StrongReference):最傳統的“引用”的定義,是指在程式程式碼之中普遍存在的引用賦值,即類似“0bject obj=new object( )”這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。
  • 軟引用(SoftReference) :在系統將要發生記憶體溢位之前,會把這些物件列入回收範圍之中,以備進行第二次(第一次指的是回收了不可觸及的垃圾物件)垃圾回收的時候回收它們。如果這次回收後還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
  • 弱引用(WeakReference) :被弱引用關聯的物件只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收掉被弱引用關聯的物件。
  • 虛引用(PhantomReference) :一個物件是否有虛引用的存在,完全不會對其生存時 間構成影響,也無法通過虛引用來獲得一個物件的例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知(回收跟蹤)。

強引用: 不回收

  • 在Java程式中,最常見的引用型別是強引用(普通系統99%以上都是強引用),也就是我們最常見的普通物件引用,也是預設的引用型別。
  • 當在Java語言中使用new操作符建立一個新的物件, 並將其賦值給一個變數的時候,這個變數就成為指向該物件的一個強引用。
  • 強引用的物件是可觸及的(可達的),垃圾收集器就永遠不會回收掉被引用的物件。
  • 對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,才可以當做垃圾被收集,當然具體回收時機還是要看垃圾收集策略。
  • 相對的,軟引用、 弱引用和虛引用的物件是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的。所以,強引用是造成Java記憶體洩漏的主要原因之一。
示例程式碼:
public class StrongReferenceTest {
    public static void main(String[] args) {
        StringBuffer str = new StringBuffer ("Hello,尚矽谷");
        StringBuffer str1 = str;

        str = null;
        System.gc();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(str1);
    }
}

StringBuffer str = new StringBuffer ("Hello,尚矽谷");
區域性變數str指向StringBuffer例項所在堆空間,通過str可以操作該例項,那麼str就是StringBuffer例項的強引用。
對應記憶體結構:

此時,如果再執行一個賦值語句:StringBuffer str1 = str;
對應記憶體結構:

本例中的兩個引用,都是強引用,強引用具備以下特點:

  • 強引用可以直接訪問目標物件。
  • 強引用所指向的物件在任何時候都不會被系統回收,虛擬機器寧願丟擲OOM異常,也不會回收強引用所指向物件。
  • 強引用可能導致記憶體洩漏。

軟引用: 記憶體不足即回收

  • 軟引用是用來描述一 些還有用,但非必需的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
  • 軟引用通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟引用。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。
  • 垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟引用,並可選地把引用存放到一個引用佇列( Reference Queue)。
  • 類似弱引用,只不過Java虛擬機器會盡量讓軟引用的存活時間長一些,迫不得.已才清理。
  • 軟引用:
    • 當記憶體足夠: 不會回收軟引|用的可達物件
    • 當記憶體不夠時: 會回收軟引用的可達物件
  • 在JDK 1. 2版之後提供了java.lang.ref.SoftReference類來實現軟引用。
Object obj = new object(); //宣告強引用
SoftReference<0bject> sf = new SoftReference<0bject>(obj);//建立軟引用
obj = null//銷燬強引用

測試程式碼:

/**
 * 軟引用的測試:記憶體不足即回收
 * -Xms10m -Xmx10m -XX:+PrintGCDetails
 */
public class SoftReferenceTest {
    public static class User {
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int id;
        public String name;

        @Override
        public String toString() {
            return "[id=" + id + ", name=" + name + "] ";
        }
    }

    public static void main(String[] args) {
        //建立物件,建立軟引用
//        SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
        //上面的一行程式碼,等價於如下的三行程式碼
        User u1 = new User(1,"songhk");
        SoftReference<User> userSoftRef = new SoftReference<User>(u1);
        u1 = null;//取消強引用


        //從軟引用中重新獲得強引用物件
        System.out.println(userSoftRef.get());

        System.gc();
        System.out.println("After GC:");
//        //垃圾回收之後獲得軟引用中的物件
        System.out.println(userSoftRef.get());//由於堆空間記憶體足夠,所有不會回收軟引用的可達物件。
//
        try {
            //讓系統認為記憶體資源緊張、不夠
//            byte[] b = new byte[1024 * 1024 * 7];
            byte[] b = new byte[1024 * 7168 - 399 * 1024];//恰好能放下陣列又放不下u1的記憶體分配大小 不會報OOM
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            //再次從軟引用中獲取資料
            System.out.println(userSoftRef.get());//在報OOM之前,垃圾回收器會回收軟引用的可達物件。列印為null
        }
    }
}

弱引用: 發現即回收

  • 弱引用也是用來描述那些非必需物件,被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。在系統GC時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的物件。
  • 但是,由於垃圾回收器的執行緒通常優先順序很低,因此,並不一 定能很快地發現持有弱引用的物件。在這種情況下,弱引用物件可以存在較長的時間。
  • 弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用佇列,就會加入指定的引用佇列,當弱引用物件被回收時,通過這個佇列可以跟蹤物件的回收情況。
  • 軟引用、弱引用都非常適合來儲存那些可有可無的快取資料。如果這麼做,當系統記憶體不足時,這些快取資料會被回收,不會導致記憶體溢位。而當記憶體資源充足時,這些快取資料又可以存在相當長的時間,從而起到加速系統的作用。
  • 在JDK1.2版之後提後了java.lang.ref.WeakReference類來實現弱引用
Object obj = new object(); //宣告強引用
WeakReference<0bject> sf = new WeakReference<0bject>(obj);//建立弱引用
obj = null//銷燬強引用
  • 弱引用物件與軟引用物件的最大不同就在於,當GC在進行回收時,需要通過演算法檢查是否回收軟引用物件,而對於弱引用物件,GC總是進行回收。弱引用物件更容易、更快被GC回收。
面試題:你開發中使用過WeakHashMap嗎?
  • 通過檢視WeakHashMap原始碼,可以看到其內部類Entry使用的就是弱引用
  • line 702 -> private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {...}
line 702 -> private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {...}

示例程式碼:

public class WeakReferenceTest {
    public static class User {
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int id;
        public String name;

        @Override
        public String toString() {
            return "[id=" + id + ", name=" + name + "] ";
        }
    }

    public static void main(String[] args) {
        //構造了弱引用
        WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
        //從弱引用中重新獲取物件
        System.out.println(userWeakRef.get());

        System.gc();
        // 不管當前記憶體空間足夠與否,都會回收它的記憶體
        System.out.println("After GC:");
        //重新嘗試從弱引用中獲取物件
        System.out.println(userWeakRef.get());//null
    }
}

虛引用: 物件回收跟蹤

  • 虛引用(Phantom Reference),也稱為“幽靈引用”或者“幻影引用”,是所有引用型別中最弱的一個。
  • 一個物件是否有虛引用的存在,完全不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。
  • 它不能單獨使用,也無法通過虛引用來獲取被引用的物件。當試圖通過虛引用的get()方法取得物件時,總是null。
  • 為一個物件設定虛引用關聯的唯一目的在於跟蹤垃圾回收過程。比如:能在這個物件被收集器回收時收到一個系統通知。
  • 虛引用必須和引用佇列一起使用。虛引用在建立時必須提供一個引用佇列作為引數。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件後,將這個虛引用加入引用佇列,以通知應用程式物件的回收情況。
  • 由於虛引用可以跟蹤物件的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。
  • 在JDK 1. 2版之後提供了PhantomReference類來實現虛引用。
object obj = new object();
ReferenceQueue phantomQueue = new ReferenceQueue( ) ;
PhantomReference<object> pf = new PhantomReference<object>(obj, phantomQueue); 
obj = null;

測試程式碼:

public class PhantomReferenceTest {
    public static PhantomReferenceTest obj;//當前類物件的宣告
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用佇列

    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {
            while (true) {
                if (phantomQueue != null) {
                    PhantomReference<PhantomReferenceTest> objt = null;
                    try {
                        objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (objt != null) {
                        System.out.println("追蹤垃圾回收過程:PhantomReferenceTest例項被GC了");
                    }
                }
            }
        }
    }

    @Override
    protected void finalize() throws Throwable { //finalize()方法只能被呼叫一次!
        super.finalize();
        System.out.println("呼叫當前類的finalize()方法");
        obj = this;
    }

    public static void main(String[] args) {
        Thread t = new CheckRefQueue();
        t.setDaemon(true);//設定為守護執行緒:當程式中沒有非守護執行緒時,守護執行緒也就執行結束。
        t.start();

        phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
        obj = new PhantomReferenceTest();
        //構造了 PhantomReferenceTest 物件的虛引用,並指定了引用佇列
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);

        try {
            //不可獲取虛引用中的物件
            System.out.println(phantomRef.get());

            //將強引用去除
            obj = null;
            //第一次進行GC,由於物件可復活,GC無法回收該物件
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj 是 null");
            } else {
                System.out.println("obj 可用");
            }
            System.out.println("第 2 次 gc");
            obj = null;
            System.gc(); //一旦將obj物件回收,就會將此虛引用存放到引用佇列中。
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj 是 null");
            } else {
                System.out.println("obj 可用");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}