JVM——(14)垃圾回收相關概念的概述

文章目錄
java

  • 往期文章
    • 前言
    • 1、System.Gc的理解
    • 2、內存溢出與內存泄露
    • 3、Stop The World
    • 4、垃圾回收的並行與併發
    • 5、安全點與安全區域
    • 6、再談引用:概述
    • 6、再談引用:強引用
    • 7、再談引用:軟引用
    • 8、再談引用:弱引用
    • 9、再談引用:虛引用
    • 10、再談引用:終結器引用
    • 參考資料
    • 單例模式
    • 一些提供close()的資源未關閉致使內存泄漏
    • Java虛擬機的堆內存設置不夠:
    • 代碼中建立了大量大對象,而且長時間不能被垃圾收集器收集(存在被引用):
    • 這裏面隱含着一層意思是`在拋出OutofMemoryError以前`,一般垃圾收集器會被觸發盡其所能去清理出空間
    • 固然也不是在任何狀況下垃圾收集器都會被觸發的
    • 內存溢出(OOM)的介紹
    • 內存溢出(OOM)緣由分析
    • 內存泄漏(Memory Leak)的介紹
    • 內存泄露的官方例子
    • 併發(Concurrent)的介紹
    • 並行(Parallel)的介紹
    • 併發與並行的對比
    • 垃圾回收的併發與並行
    • 實際執行時
    • 如何在GC發生時,全部線程都跑到最近的安全點停頓下來呢?
    • 安全點介紹
    • 安全區域介紹
    • 強引用(StrongReference):
    • 軟引用(SoftReference):
    • 弱引用(WeakReference)
    • 虛引用(PhantomReference):
    • 在JDK1.2版以後提供了SoftReference類來實現軟引用
    • 在JDK1.2版以後提供了WeakReference類來實現弱引用
    • 在JDK1.2版以後提供了PhantomReference類來實現虛引用


往期文章

JVM——(1)爲何學習虛擬機
JVM——(2)聊聊JVM虛擬機
JVM——(3)類加載子系統
JVM——(4)運行時數據區的概述與程序計數器(PC寄存器)
JVM——(5)運行時數據區的虛擬機棧
JVM——(6)運行時數據區的本地方法棧
JVM——(7)運行時數據區的堆空間
JVM——(8)運行時數據區的方法區
JVM——(9)對象的實例化與訪問定位
JVM——(10)執行引擎
JVM——(11)String Table(字符串常量池)
JVM——(12)垃圾回收概述
JVM——(13)垃圾回收相關算法
JVM——(14)垃圾回收相關概念的概述
JVM——(15)垃圾回收器詳細篇
JVM——(16)Class文件結構一(描述介紹)
JVM——(17)Class文件結構二(解讀字節碼)
JVM——(18)Class文件結構三(JAVAP指令)
JVM——(19)字節碼指令集與解析一(局部變量壓棧、常量變量壓棧、出棧局部變量表指令)
JVM——(20)字節碼指令集與解析二(算數指令)
JVM——(21)字節碼指令集與解析三(類型轉換指令)
JVM——(22)字節碼指令集與解析四(對象建立與訪問指令)
JVM——(23)字節碼指令集與解析五(方法調用指令與方法返回指令)
JVM——(24)字節碼指令集與解析六(操做數棧管理指令)面試

前言

從本篇開始咱們正式講解垃圾回收的相關知識了,讓咱們開始吧算法

1、System.Gc的理解


在默認狀況下經過System.gc()者Runtime.getRuntime().gc() 的調用,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄對象佔用的內存數據庫

然而System.gc()調用附帶一個免責聲明,沒法保證對垃圾收集器的調用(不能確保當即生效)數組

簡單的意思說提醒JVM進行垃圾回收,但不能保證垃圾收集器它進行調用緩存

JVM實現者能夠經過System.gc() 調用來決定JVM的GC行爲。而通常狀況下垃圾回收應該是自動進行的,**無須手動觸發,不然就太過於麻煩了。**在一些特殊狀況下,若是咱們正在編寫一個性能基準,咱們能夠在運行之間調用System.gc()安全

接下來咱們使用示例代碼演示一下網絡

public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc();//提醒jvm的垃圾回收器執行gc,可是不肯定是否立刻執行gc
    }
    //若是發生了GC,這個finalize()必定會被調用
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重寫了finalize()");
    }

輸出結果不肯定:有時候會調用 finalize() 方法,有時候並不會調用併發

而且咱們調用的System.gc()背後調用又是Runtime.getRuntime().gc()方法jvm

public final class System {

    public static void gc() {
        Runtime.getRuntime().gc();
    }}public class Runtime {

    public native void gc();}

咱們再來看一個示例代碼,採用強制調用的方式

public class SystemGCTest {
    public static void main(String[] args) {
       new SystemGCTest();
       System.runFinalization();//強制調用使用引用的對象的finalize()方法
    }
    //若是發生了GC,這個finalize()必定會被調用
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重寫了finalize()");
    }}//運行結果以下:SystemGCTest 重寫了finalize()

那麼這時咱們再運行程序,就能夠強制調用使用引用的對象的finalize()方法

這時咱們再經過代碼示例來,體會手動調用 GC 來理解 不可達對象的回收行爲

public class LocalVarGC {
    
    public void localvarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];//10MB
        System.gc();
    }

    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
        //經過在main方法調用這個方法進行測試
        local.localvarGC1();
    }}

這時咱們設置一下JVM的參數,而且運行項目看看具體信息如何
在這裏插入圖片描述
在這裏插入圖片描述
這時咱們完成了第一個示例代碼的狀況分析,接下來進行第二個示例代碼狀況分析

public class LocalVarGC {
        
    public void localvarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
    }
    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
        //經過在main方法調用這幾個方法進行測試
        local.localvarGC2();
    }}

與上面的JVM參數設置一致,這時咱們運行起來程序看看具體信息怎麼樣
在這裏插入圖片描述

這時咱們完成了第二個示例代碼的狀況分析,接下來進行第三個示例代碼狀況分析

public class LocalVarGC {

    public void localvarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }
    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
        //經過在main方法調用這個方法進行測試
        local.localvarGC3();
    }}

與上面的JVM參數設置一致,這時咱們運行起來程序看看具體信息怎麼樣
在這裏插入圖片描述
在這裏插入圖片描述

這時咱們完成了第三個示例代碼的狀況分析,接下來進行第四個示例代碼狀況分析

public class LocalVarGC {
    
    public void localvarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();
    }
    
    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
        //經過在main方法調用這個方法進行測試
        local.localvarGC4();
    }}

與上面的JVM參數設置一致,這時咱們運行起來程序看看具體信息怎麼樣
在這裏插入圖片描述
在這裏插入圖片描述
這時咱們完成了第四個示例代碼的狀況分析,接下來進行第五個示例代碼狀況分析

public class LocalVarGC {
    
    public void localvarGC5() {
        localvarGC1();
        System.gc();
    }

    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
        //經過在main方法調用這幾個方法進行測試
        local.localvarGC5();
    }}

與上面的JVM參數設置一致,這時咱們運行起來程序看看具體信息怎麼樣
在這裏插入圖片描述

2、內存溢出與內存泄露


內存溢出(OOM)的介紹

================================
內存溢出相對於內存泄漏來講,儘管更容易被理解可是一樣的,內存溢出也是引起程序崩潰的罪魁禍首之一

因爲GC一直在發展全部通常狀況下,除非應用程序佔用的內存增加速度很是快,形成垃圾回收已經跟不上內存消耗的速度,不然不太容易出現OOM的狀況

大多數狀況下GC會進行各類年齡段的垃圾回收,實在不行了就放大招,來一次獨佔式的Full GC操做,這時候會回收大量的內存供應用程序繼續使用

Javadoc中對OutofMemoryError的解釋是:沒有空閒內存,而且垃圾收集器也沒法提供更多內存

內存溢出(OOM)緣由分析

================================

首先說沒有空閒內存的狀況:說明Java虛擬機的堆內存不夠。緣由有二:

Java虛擬機的堆內存設置不夠:

好比:可能存在內存泄漏問題,也頗有可能就是堆的大小不合理,好比咱們要處理比較可觀的數據量,可是沒有顯式指定JVM堆大小或者指定數值偏小。咱們能夠經過參數-Xms 、-Xmx來調整

代碼中建立了大量大對象,而且長時間不能被垃圾收集器收集(存在被引用):

對於老版本的Oracle JDK永久代的大小是有限的,而且JVM對永久代垃圾回收(如,常量池回收、卸載再也不須要的類型)很是不積極,因此當咱們不斷添加新類型的時候,永久代出現OutOfMemoryError也很是多見

尤爲是在運行時存在大量動態類型生成的場合;相似intern字符串緩存佔用太多空間也會致使OOM問題

對應的異常信息會標記出來和永久代相關:「java.lang.OutOfMemoryError:PermGen space」

隨着元數據區的引入,方法區內存已經再也不那麼窘迫,因此相應的OOM有所改觀,出現OOM、異常信息則變成了:「java.lang.OutofMemoryError:Metaspace」。直接內存不足,也會致使OOM

這裏面隱含着一層意思是在拋出OutofMemoryError以前,一般垃圾收集器會被觸發盡其所能去清理出空間
  • 例如:在引用機制分析中,涉及到JVM會去嘗試回收軟引用指向的對象等。
  • 在java.nio.Bits.reserveMemory()方法中,能清楚的看到System.gc()會被調用以清理空間。
固然也不是在任何狀況下垃圾收集器都會被觸發的

好比咱們去分配一個超大對象,相似一個超大數組超過堆的最大值,JVM能夠判斷出垃圾收集並不能解決這個問題,因此直接拋出OutofMemoryError

內存泄漏(Memory Leak)的介紹

================================

內存泄漏也稱做「存儲滲漏」

嚴格來講只有對象不會再被程序用到了,可是GC又不能回收他們的狀況,才叫內存泄漏

但實際狀況不少時候一些不太好的實踐(或疏忽)會致使對象的生命週期變得很長甚至致使OOM,也能夠叫作寬泛意義上的「內存泄漏」

儘管內存泄漏並不會馬上引發程序崩潰,可是一旦發生內存泄漏,程序中的可用內存就會被逐步蠶食,直至耗盡全部內存,最終出現OutofMemory異常,致使程序崩潰

注意:這裏的存儲空間並非指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決於磁盤交換區設定的大小

內存泄露的官方例子

================================
在這裏插入圖片描述
在這裏插入圖片描述

在咱們的程序中常見的例子有:

單例模式

單例的生命週期和應用程序是同樣長的,因此在單例程序中若是持有對外部對象的引用的話,那麼這個外部對象是不能被回收的,則會致使內存泄漏的產生

一些提供close()的資源未關閉致使內存泄漏

數據庫鏈接 dataSourse.getConnection(),網絡鏈接socket和io鏈接必須手動close,不然是不能被回收的

3、Stop The World


Stop-the-World簡稱STW,指的是GC事件發生過程當中,會產生應用程序的停頓。停頓產生時整個應用程序線程都會被暫停,沒有任何響應,有點像卡死的感受,這個停頓稱爲STW

可達性分析算法中枚舉根節點(GC Roots)會致使全部Java執行線程停頓

那麼爲何須要停頓全部 Java 執行線程呢?有如下幾種緣由

  • 分析工做必須在一個能確保一致性的快照中進行
  • 一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上
  • 若是出現分析過程當中對象引用關係還在不斷變化,則分析結果的準確性沒法保證

而被STW中斷的應用程序線程會在完成GC以後恢復,頻繁中斷會讓用戶感受像是網速不快形成電影卡帶同樣,因此咱們須要減小STW的發生

不過須要提醒的是:STW事件和採用哪款GC無關,全部的GC都有這個事件

哪怕是G1也不能徹底避免Stop-the-world狀況發生,只能說垃圾回收器愈來愈優秀,回收效率愈來愈高,儘量地縮短了暫停時間

STW是JVM在後臺自動發起和自動完成的。在用戶不可見的狀況下,把用戶正常的工做線程所有停掉

因此咱們在開發中不要用System.gc() ,這會致使Stop-the-World的發生

接下來咱們使用示例代碼體會體會STW是什麼感覺

public static class PrintThread extends Thread {
    
    public final long startTime = System.currentTimeMillis();

    public void run() {
        try {
            while (true) {
                // 每秒打印時間信息
                long t = System.currentTimeMillis() - startTime;
                System.out.println(t / 1000 + "." + t % 1000);
                Thread.sleep(1000);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args) {
        PrintThread p = new PrintThread();
        p.start();
    }}//運行結果以下:0.11.12.1.....

咱們能夠觀察得出結果:當前時間間隔與上次時間間隔**基本**是每隔1秒打印一次

這時咱們再添加多一個線程作事情,當知足某些條件的時候執行gc

public static class WorkThread extends Thread {
    
    List<byte[]> list = new ArrayList<byte[]>();

    public void run() {
        try {
            while (true) {
                for(int i = 0;i < 1000;i++){
                    byte[] buffer = new byte[1024];
                    list.add(buffer);
                }
                if(list.size() > 10000){
                    list.clear();
                    System.gc();//會觸發full gc,進而會出現STW事件
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }}

這時咱們新建一個Demo類將這兩個類執行起來,看看gc對咱們原有輸出有什麼影響

public class StopTheWorldDemo {
    WorkThread w = new WorkThread();
    PrintThread p = new PrintThread();
    w.start();
    p.start();}//運行結果以下:0.01.72.83.104.115.13

咱們能夠觀察得出結果:當前時間間隔與上次時間間隔相差 1.3s 出現Stop the World的感受

4、垃圾回收的並行與併發


併發(Concurrent)的介紹

================================

在操做系統中指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理器上運行

併發不是真正意義上的「同時進行」,只是CPU把一個時間段劃分紅幾個時間片斷(時間區間),而後在這幾個時間區間之間來回切換。因爲CPU處理的速度很是快,只要時間間隔處理得當,便可讓用戶感受是多個應用程序同時在進行
在這裏插入圖片描述

並行(Parallel)的介紹

================================

當系統有一個以上CPU時,當一個CPU執行一個進程時,另外一個CPU能夠執行另外一個進程,兩個進程互不搶佔CPU資源,能夠同時進行,咱們稱之爲並行(Parallel)

其實決定並行的因素不是CPU的數量,而是CPU的核心數量,好比一個CPU多個核也能夠並行
在這裏插入圖片描述

併發與並行的對比

================================

併發,指的是多個事情,在同一時間段內同時發生了。

並行,指的是多個事情,在同一時間點上(或者說同一時刻)同時發生了。

併發的多個任務之間是互相搶佔資源的。並行的多個任務之間是不互相搶佔資源的。

只有在多CPU或者一個CPU多核的狀況中,纔會發生並行。不然看似同時發生的事情,其實都是併發執行的。

垃圾回收的併發與並行

================================

並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍處於等待狀態

串行(Serial):相較於並行的概念,單線程執行。若是內存不夠,則程序暫停,啓動JVM垃圾回收器進行垃圾回收(單線程)
在這裏插入圖片描述

併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),垃圾回收線程在執行時不會停頓用戶程序的運行
在這裏插入圖片描述

5、安全點與安全區域


安全點介紹

================================

程序執行時並不是在全部地方都能停頓下來開始GC,只有在特定的位置才能停頓下來開始GC,這些位置稱爲「安全點(Safepoint)」

Safe Point的選擇很重要,若是太少可能致使GC等待的時間太長,若是太頻繁可能致使運行時的性能問題

大部分指令的執行時間都很是短暫,一般會根據「是否具備讓程序長時間執行的特徵」爲標準

好比:選擇一些執行時間較長的指令做爲Safe Point,如方法調用、循環跳轉和異常跳轉等

如何在GC發生時,全部線程都跑到最近的安全點停頓下來呢?

搶先式中斷:(目前沒有虛擬機採用了)首先中斷全部線程。若是還有線程不在安全點,就恢復線程,讓線程跑到安全點

主動式中斷:設置一箇中斷標誌,各個線程運行到Safe Point的時候主動輪詢這個標誌,若是中斷標誌爲真,則將本身進行中斷掛起

安全區域介紹

================================

安全區域是指在一段代碼片斷中,對象的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。咱們也能夠把Safe Region看作是被擴展了的Safepoint

咱們說Safepoint 機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。可是,程序「不執行」的時候呢?

例如線程處於Sleep狀態或Blocked 狀態,這時候線程沒法響應JVM的中斷請求,「走」到安全點去中斷掛起,JVM也不太可能等待線程被喚醒。對於這種狀況,就須要安全區域(Safe Region)來解決

實際執行時

當線程運行到Safe Region的代碼時,首先標識已經進入了Safe Region,若是這段時間內發生GC,JVM會忽略標識爲Safe Region狀態的線程

當線程即將離開Safe Region時,會檢查JVM是否已經完成GC,若是完成了則繼續運行,不然線程必須等待直到收到能夠安全離開Safe Region的信號爲止

6、再談引用:概述


咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存中;若是內存空間在進行垃圾收集後仍是很緊張,則能夠拋棄這些對象

那麼要想描述這樣一類的對象就要講講引用:強引用、軟引用、弱引用、虛引用

先看一道偏門高頻的面試題:強引用、軟引用、弱引用、虛引用有什麼區別?具體使用場景是什麼?

在JDK1.2版以後,Java對引用的概念進行了擴充,將引用分爲:

  • 強引用(Strong Reference)
  • 軟引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虛引用(Phantom Reference)

這4種引用強度依次逐漸減弱。除強外其餘3種引用都可以在java.lang.ref包中找到它們的身影
在這裏插入圖片描述
Reference子類中只有終結器引用是包內可見的,其餘3種引用類型均爲public,能夠在應用程序中直接使用

接下來咱們概述行的說明一下這些引用是什麼意思

強引用(StrongReference):

最傳統的「引用」的定義是指在程序代碼之中廣泛存在的引用賦值,即相似「object obj=new Object()」這種引用關係

不管任何狀況下只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。寧肯報OOM,也不會GC強引用

軟引用(SoftReference):

在系統將要發生內存溢出以前,將會把這些對象列入回收範圍之中進行第二次回收

若是此次回收後尚未足夠的內存,纔會拋出內存溢出異常

弱引用(WeakReference)

被弱引用關聯的對象只能生存到下一次垃圾收集以前。當垃圾收集器工做時,不管內存空間是否足夠,都會回收掉被弱引用關聯的對象

虛引用(PhantomReference):

一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來得到一個對象的實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知

6、再談引用:強引用


在Java程序中,最多見的引用類型是強引用(普通系統99%以上都是強引用),也就是咱們最多見的普通對象引用,也是默認的引用類型

當在Java語言中使用new操做符建立一個新的對象,並將其賦值給一個變量的時候,這個變量就成爲指向該對象的一個強引用

只要強引用的對象是可觸及的,垃圾收集器就永遠不會回收掉被引用的對象。只要強引用的對象是可達的,jvm寧肯報OOM,也不會回收強引用

接下來咱們使用一個示例代碼體會一下強引用形成的輸出

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);
    }}//輸出結果以下:Hello,小明

在上面代碼中局部變量str指向stringBuffer實例所在堆空間,經過str能夠操做該實例,那麼str就是stringBuffer實例的強引用對應內存結構
在這裏插入圖片描述
在上面的代碼中咱們能夠知道強引用大概有如下幾點特色:

  • 強引用能夠直接訪問目標對象。
  • 強引用所指向的對象在任什麼時候候都不會被系統回收,虛擬機寧願拋出OOM異常,也不會回收強引用所指向對象。
  • 強引用可能致使內存泄漏

因此對於一個普通的對象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲null,就是能夠當作垃圾被收集了,固然具體回收時機仍是要看垃圾收集策略

相對的,軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及的,在必定條件下,都是能夠被回收的。因此,強引用是形成Java內存泄漏的主要緣由之一

7、再談引用:軟引用


軟引用是用來描述一些還有用但非必需的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。注意:這裏的第一次回收是不可達的對象

通常咱們軟引用一般用來實現內存敏感的緩存。好比:高速緩存就有用到軟引用。若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存

在JDK1.2版以後提供了SoftReference類來實現軟引用
Object obj = new Object();// 聲明強引用SoftReference<Object> sf = new SoftReference<>(obj);obj = null; //銷燬強引用

垃圾回收器在某個時刻決定回收軟可達的對象的時候會清理軟引用,並可選地把引用存放到一個引用隊列(Reference Queue)

相似弱引用,只不過Java虛擬機會盡可能讓軟引用的存活時間長一些,無可奈何才清理

接下來咱們使用示例代碼體會體會軟引用是什麼狀況

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 + "] ";
    }}

此時添加main方法,運行程序看看是什麼結果

public static void main(String[] args) {
    
    User u1 = new User(1,"songhk");
    SoftReference<User> userSoftRef = new SoftReference<User>(u1);
    u1 = null;//取消強引用

    //從軟引用中從新得到強引用對象
    System.out.println(userSoftRef.get());}運行結果以下:[id = 1,name = songhk]

如今咱們正常運行輸出沒有什麼問題,此時咱們設置堆空間大小並運行System.gc後看看是否還在
在這裏插入圖片描述

public static void main(String[] args) {
    //建立對象,創建軟引用
    //SoftReferenceuserSoftRef = new SoftReference(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());//因爲堆空間內存足夠,全部不會回收軟引用的可達對象。}//運行結果以下:[id = 1,name = songhk]After GC:[id = 1,name = songhk]

此時堆空間仍是足夠的,並無回收軟引用,此時咱們操做一下將內存資源緊張緊張

public static void main(String[] args) {
    
    User u1 = new User(1,"songhk");
    SoftReference<User> userSoftRef = new SoftReference<User>(u1);
    u1 = null;//取消強引用

    //從軟引用中從新得到強引用對象
    System.out.println(userSoftRef.get());
    System.out.println("---目前內存還不緊張---");
    System.gc();
    System.out.println("After GC:");
    //垃圾回收以後得到軟引用中的對象
    System.out.println(userSoftRef.get());
    System.out.println("---下面開始內存緊張了---");
    try {
        //讓系統認爲內存資源緊張、不夠
        byte[] b = new byte[1024 * 1024 * 7];
    } catch (Throwable e) {
        e.printStackTrace();
    } finally {
        //再次從軟引用中獲取數據
        System.out.println(userSoftRef.get());
    }}//運行結果以下:[id=1, name=songhk] ---目前內存還不緊張---After GC:[id=1, name=songhk] ---下面開始內存緊張了---null
java.lang.OutOfMemoryError: Java heap space
 at com.atguigu.java1.SoftReferenceTest.main(SoftReferenceTest.java:48)

此時咱們就能夠看到在堆空間不報OOM時,垃圾回收期會回收軟引用的可達對象

一句話歸納:當內存足夠時,不會回收軟引用可達的對象。內存不夠時,會回收軟引用的可達對象

8、再談引用:弱引用


弱引用也是用來描述那些非必需對象,被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止。在系統GC時,只要發現弱引用,無論系統堆空間使用是否充足,都會回收掉只被弱引用關聯的對象

可是因爲垃圾回收器的線程一般優先級很低,所以並不必定能很快地發現持有弱引用的對象。在這種狀況下,弱引用對象能夠存在較長的時間

弱引用和軟引用同樣在構造弱引用時,也能夠指定一個引用隊列,當弱引用對象被回收時就會加入指定的引用隊列,經過這個隊列能夠跟蹤對象的回收狀況

在JDK1.2版以後提供了WeakReference類來實現弱引用
// 聲明強引用Object obj = new Object();WeakReference<Object> sf = new WeakReference<>(obj);obj = null; //銷燬強引用

弱引用對象與軟引用對象的最大不一樣就在於,當GC在進行回收時須要經過算法檢查是否回收軟引用對象,而對於弱引用對象,GC老是進行回收。弱引用對象更容易、更快被GC回收

接下來咱們經過示例來體會一下弱引用

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 + "] ";
    }}

這時咱們添加main方法調用這個User類,看看咱們gc後還存在不

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());}//運行結果以下:[id=1, name=songhk] After GC:null

這時咱們就能夠知道執行垃圾回收後,軟引用對象一定被清除

軟引用、弱引用都很是適合來保存那些無關緊要的緩存數據。若是這麼作,當系統內存不足時,這些緩存數據會被回收,不會致使內存溢出。而當內存資源充足時,這些緩存數據又能夠存在至關長的時間,從而起到加速系統的做用

9、再談引用:虛引用


虛引用也稱爲「幽靈引用」或者「幻影引用」,是全部引用類型中最弱的一個

一個對象是否有虛引用的存在,徹底不會決定對象的生命週期。

若是一個對象僅持有虛引用,那麼它和沒有引用幾乎是同樣的,隨時均可能被垃圾回收器回收

它不能單獨使用,也沒法經過虛引用來獲取被引用的對象。當試圖經過虛引用的get()方法取得對象時,老是null 。即經過虛引用沒法獲取到咱們的數據

一個對象設置虛引用關聯的惟一目的在於跟蹤垃圾回收過程。好比:能在這個對象被收集器回收時收到一個系統通知

在JDK1.2版以後提供了PhantomReference類來實現虛引用
// 聲明強引用Object obj = new Object();// 聲明引用隊列ReferenceQueue phantomQueue = new ReferenceQueue();// 聲明虛引用(還須要傳入引用隊列)PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);obj = null;

咱們使用示例體會一下虛引用是什麼,先體會看看可否獲取

public class PhantomReferenceTest {

    public static PhantomReferenceTest obj;//當前類對象的聲明
    
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用隊列
    
    public static void main(String[] args) {
    
        phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
        
        obj = new PhantomReferenceTest();
    
        //構造了 PhantomReferenceTest 對象的虛引用,並指定了引用隊列
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
        
        System.out.println(phantomRef.get());
    
    }}//運行結果以下:null

第一次嘗試獲取虛引用的值,發現沒法獲取的,這是由於虛引用是沒法直接獲取對象的值

接下來咱們添加一些代碼進來,再看看是個什麼狀況

public class PhantomReferenceTest {
    
    public static PhantomReferenceTest obj;//當前類對象的聲明
    
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用隊列
    
    @Override
    protected void finalize() throws Throwable { //finalize()方法只能被調用一次!
        super.finalize();
        System.out.println("調用當前類的finalize()方法");
        obj = this;
    }}public static void main(String[] args) {
    
    phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
    obj = new PhantomReferenceTest();
    
    //構造了 PhantomReferenceTest 對象的虛引用,並指定了引用隊列
    PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
  
    System.out.println(phantomRef.get());

    //將強引用去除
    obj = null;
    System.gc();
    Thread.sleep(1000);
    if (obj == null) {
        System.out.println("obj 是 null");
    } else {
        System.out.println("obj 可用");
    }}//運行結果以下:null
第 1 次 gc
調用當前類的finalize()方法
obj 可用

此時咱們進行第一次GC,由於會調用finalize方法,將對象復活了,因此對象沒有被回收。

那麼這關咱們虛引用什麼事呢?不急讓咱們再添加一些代碼說明一下

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了");
                    }
                }
            }
        }
    }}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());
        System.out.println("第 1 次 gc");
        //將強引用去除
        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();
    }}

咱們main方法裏啓動一個線程進行虛引用Object對象的判斷,一直while循環進行

當調用第二次GC操做的時候,由於finalize方法只能執行一次,因此就觸發了GC操做,將對象回收了,同時將會觸發第二個操做就是將待回收的對象存入到引用隊列中

因此此時的輸出結果就是

null
第 1 次 gc
調用當前類的finalize()方法
obj 可用
第 2 次 gc
追蹤垃圾回收過程:PhantomReferenceTest實例被GC了
obj 是 null

10、再談引用:終結器引用


它用於實現對象的finalize() 方法,也能夠稱爲終結器引用

無需手動編碼,其內部配合引用隊列使用

在GC時,終結器引用入隊。由Finalizer線程經過終結器引用找到被引用對象調用它的finalize()方法,第二次GC時纔回收被引用的對象

相關文章
相關標籤/搜索