1. 程式人生 > 實用技巧 >[JVM垃圾回收3]垃圾回收相關概念

[JVM垃圾回收3]垃圾回收相關概念

System.gc()的理解

  • 在預設情況下,通過System.gc()或Runtime.getRuntime().gc()的呼叫,會顯示觸發FullGC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件佔用的記憶體。
  • 然而System.gc()呼叫附帶一個免責宣告,無法保證對垃圾收集器的呼叫。
  • JVM實現者可以通過System.gc()顯示來決定JVM的GC行為。而一般情況下,垃圾收集應該是自動進行的,無需手動觸發,否則就太過麻煩。在一些特殊情況下,比如編寫一個性能基準,可在執行前呼叫System.gc()。
public class SystemGCTest {

    public
static void main(String[] args) { new SystemGCTest(); System.gc();//提醒JVM的垃圾回收器執行GC,但不確定馬上執行。 //與 Runtime.getRuntime().gc()的作用一致。 //System.runFinalization();//強制呼叫引用物件的finalize()方法 } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(
"SystemGCTest 重寫了 finalize()"); } }

LocalVarGC例項程式碼:

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

    public void localVarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
    }

    
public void localVarGC3() { { byte[] buffer = new byte[10 * 1024 * 1024]; } System.gc(); } //並沒有回收 //為了儘可能節省棧幀空間,區域性變量表的slot是可以複用的 //推薦編碼規則:把不使用的物件手動置為null public void localVarGC4() { { byte[] buffer = new byte[10 * 1024 * 1024]; } int value = 10; System.gc(); } public void localVarGC5() { localVarGC1();//回收 System.gc(); } public static void main(String[] args) { LocalVarGC local = new LocalVarGC(); local.localVarGC4(); } }

記憶體溢位與記憶體洩漏

記憶體溢位(OOM)

  • 記憶體溢位相對於記憶體洩漏來說,更容易被理解,但是同樣的,記憶體溢位也是引發程式崩潰的罪魁禍首之一。

  • 由於GC一直在發展,所以一般情況下,除非應用程式佔用的記憶體增長速度非常快,造成垃圾收集器跟不上記憶體消耗的速度,否則不太容易出現OOM的情況。

  • 大多數情況下,GC會進行各種年齡段的垃圾回收,實在不行就放大招來一次獨佔式的Full GC操作,這時候會回收大量記憶體,供應用程式繼續使用。

  • javadoc中對OutOfMemoryError的解釋是。沒有空閒記憶體,並且垃圾收集器也無法提供更多記憶體

  • 內有空閒記憶體的情況,說明Java虛擬機器內的堆記憶體不夠,原因有二:

    1. Java虛擬機器堆記憶體設定不夠
      比如:可能存在記憶體洩漏問題,也很有可能就是堆記憶體大小設定不合理,比如我們要處理比較可觀的資料量,但是沒有顯示指定JVM堆大小或者指定數值偏小。可以通過 -Xms -Xmx來調整。
    2. 程式碼中建立了大量物件,並且長時間不能被垃圾收集器收集(存在並引用)
      對於老版本的Oracle JDK,因為永久代大小有限,並且JVM堆永久代回收(如常量池回收,解除安裝不需要的型別)非常不積極,所以不斷新增新型別時,永久代出現OutOfMemoryError也非常多,尤其是執行時存在大量動態型別生成的場合;類似intern字串快取佔用太多空間也會導致OOM問題,對應的異常資訊。會標記出和永久代相關:“java.lang.OutOfMemoryError: PermGen space”
      隨著元資料區的引入,方法區記憶體已經不那麼窘迫,所以相應的OOM有所改觀,出現OOM,異常資訊變成了“java.lang.OutOfMemoryError:Metaspace"。直接記憶體不足,也會導致OOM。
  • 隱含一層意思是:在丟擲OOM之前,通常垃圾收集器會被觸發,儘可能去清理出空間。

    • 例如:在引用機制分析中,涉及到JVM回去嘗試回收軟引用指向的物件等
    • 在java.noi.BIts.reserveMemory()方法中,可以清楚看到system.gc()會被呼叫,以清理空間。
  • 當然,也不是任何情況下垃圾收集器都會觸發

    • 比如,去分配一個超大物件,類似一個超大陣列超過堆的最大值,JVM可以判斷垃圾收集器不能解決這個問題,直接OOM。

記憶體洩漏(Memory Leak)

也稱作“儲存滲漏”。嚴格來說,只有物件不再被程式用到了,但是GC又不能夠回收它們的情況,才叫記憶體洩漏

但實際情況很多時候一些不太好實踐(或疏忽)會導致物件生命週期變得很長甚至導致OOM,也可以叫做寬泛意義上的“記憶體洩漏”

儘管記憶體洩漏並不會立刻引起程式崩潰,但是一旦發生記憶體洩漏,程式中的可用記憶體就會被逐步蠶食,直到耗盡記憶體,最終出現OOM異常,導致程式崩潰。

注意,這裡的儲存空間並不是指實體記憶體,而是虛擬機器記憶體大小,這個虛擬記憶體大小取決於磁碟交換區設定的大小。

舉例:

  1. 單例模式
    單例的生命週期和應用程式是一樣長的,所以單例程式中,如果持有對外部物件的引用的話,那麼這個外部物件是不能被回收的,則會導致記憶體洩漏的產生。

  2. 一些提供close的資源未關閉導致記憶體洩漏
    資料庫連線(dataSource。getConnection()),網路連線Socket和io連線必須手動close,否則不能被回收。

注意:迴圈引用記憶體洩漏例子不使用與Java,因為Java使用可達分析演算法。

Stop The World

  • Stop-The-World,簡稱STW,指的是GC事件發生過程中,會產生應用程式的停頓。停頓產生時整個應用程式執行緒都會被暫停,沒有任何響應,有點像卡死狀態,這個停頓稱為STW。
    • 可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。
      ->分析工作必須在一個能確保一致性的快照中進行
      ->一致性指整個分析期間整個執行系統看起來都像被凍結在某個時間點上
      ->如果出現分析過程中物件引用關係還在不斷變化,則分析結果的準確性無法保證
  • 被STW中斷的應用執行緒會在完成GC之後恢復,頻繁中斷會讓使用者感覺像是網速不快造成電影卡帶一樣,所以需要減少STW的發生。
  • STW事件和採用哪種GC無關,所有的GC都有這個事件。
  • 哪怕是G1也不能完全避免STW情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,儘可能縮短了暫停時間。
  • STW是JVM在後臺自動發起和自動完成的。在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉。
  • 開發中不要用System.gc(),會導致STW的發生。

垃圾回收的並行與併發

併發

  • 在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理器上執行。
  • 併發並不是真正意義上的“同時進行”,只是CPU把一個時間段劃分成幾時間片段(時間區間),然後在這幾個時間區間之間來回切換,由於CPU處理速度非常快,只要時間間隔處理得當,即可讓使用者感覺是多個應用程式同時進行。

並行

  • 當系統有一個以上CPU時,當一個CPU執行一個程序時,另一個CPU可以執行另一個程序,兩個程序互不搶佔CPU資源,可以用時進行,稱之為並行(Parallel)。
  • 決定並行的因素並不是CPU的數量,而是核心的數量。
  • 適合科學計算,後臺處理等弱互動場景。

垃圾回收的並行與併發

  • 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態。
    • 如ParNew、Parallel Scavenge、Parallel Old
  • 序列(Serial)
    • 相較於並行的概念,單執行緒執行。
    • 如果記憶體不夠,則程式暫停,啟動JVM垃圾回收器進行垃圾回收。回收完,再啟動程式的執行緒。

  • 併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行(但並不一定是並行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓使用者程式的執行。
    • 使用者程式在繼續執行,而垃圾收集程式執行緒運行於另一個CPU上。
    • 如:CMS、G1

安全點與安全區域

程式在執行時並非在所有地方都能停頓下來開始GC,只有在特定的位置才能停頓下來開始GC,這些位置稱為“安全點(Safepoint)”。

Safe Point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致執行時的效能問題 大部分指令的執行時間都非常短暫,通常會根據 是否具有讓程式長時間執行的特徵 為標準。比如:選擇一些執行時間較長的指令作為SafePoint,如方法呼叫、迴圈跳轉和異常跳轉等。

再談引用

在Jdk1.2版之後,Java堆引用概念進行了擴充,降引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Week Reference)和虛引用(Phantom Reference)4種。這4種引用強度依次逐漸減弱

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

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

強引用

  • 在Java程式中,最常見的引用型別就是強引用(普通系統99%以上都是強引用),也就是最常見的普通引用,也是預設的引用型別

  • 在Java語言中使用new操作符建立一個新的物件,並將其賦值給一個變數的時候,這個變數就成為指向該物件的一個強引用。

  • 強引用的物件是可觸及的,垃圾收集器就永遠不會回收被引用的物件

  • 對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯示地將引用賦值為null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。

  • 相對的,軟引用、弱引用和虛引用的物件是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都可以被回收。所以,強引用是造成Java記憶體洩漏的主要原因之一

  • 強引用具備以下特點:

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

軟引用

  • 軟引用是用來描述一些還有用,但非必需的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行二次回收,如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。

  • 軟引用通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟引用。如果記憶體還有空閒,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。

  • 垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟引用,並可選地吧引用存放到一個引用佇列(Reference Queue)。

  • 類似弱引用,只不過Java虛擬機器會盡量讓軟引用存活時間長一些,迫不得已才清理。

  • 總結:

    • 當記憶體足夠:不會回收軟引用的可達物件
    • 當記憶體不夠:會回收軟引用的可達物件
Object obj = new Object();//宣告強引用
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;//銷燬強引用

軟引用測試程式碼:

public class SoftReferenceTest {

    public static class User {
        private String id;
        private String name;

        public User(String id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id='" + id + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }


    //-Xms=10m  -Xmx=10m
    public static void main(String[] args) {
        //建立物件,建立軟引用
        //SoftReference<User> userSoftRef = new SoftReference<User>(new User("1", "jack"));
        User u1 = new User("1", "Jack");
        SoftReference<User> userSoftRef = new SoftReference<User>(u1);
        u1 = null;//取消強引用

        //從軟引用中重新獲取強引用物件
        System.out.println(userSoftRef.get());//由於堆空間記憶體足夠,不會回收軟引用。

        System.gc();

        try {
            //讓系統認為記憶體緊張
            byte[] b = new byte[1024 * 1024 * 7];
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            //再次從軟引用獲得資料
            System.out.println(userSoftRef.get());//在OOM之前,垃圾回收器會回收軟引用的可達物件
        }

    }

}

弱引用(Week Reference)—發現即回收

  • 弱引用也是用來描述那些非必須物件,被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。在系統GC時,只要發現弱引用,不管系統堆記憶體空間是否充足,都會回收掉被弱引用關聯的物件。

  • 但是,由於垃圾收集器的執行緒通常優先順序很低,因此,並不一定很快地發現持有弱引用的物件。在這種情況下,弱引用物件可以存在較長的時間

  • 弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用佇列,當弱引用物件被回收時,就會加入指定的引用佇列,通過這個佇列可以跟蹤物件的回收情況。

  • 弱引用,弱引用都非常適合儲存那些可有可無的快取資料。如果這麼做,當系統記憶體不足時,這些快取資料就會被回收,不會導致記憶體溢位。而當記憶體資源充足時,這些快取資料又可以存在相當長的時間,從而起到加速系統作用。

  • 弱引用物件與軟引用物件最大的不同就在於,當GC在進行回收時,需要通過演算法檢查是否回收軟引用物件,而對於弱引用物件,GC總是進行回收。弱引用物件更容易、更快被GC回收

問題:WeekHashMap的使用?

虛引用(Phantom Reference)—物件回收跟蹤

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

終結器引用(Final Reference)

  • 它用以實現物件的finalize()方法,也可以成為終結器引用
  • 無需手動編碼,其內部配合引用佇列使用
  • 在GC時,終結器引用入隊。由Finalizer執行緒通過終結器引用找到被引用的物件並呼叫finalize()方法,第二次GC時才能回收被引用的物件。