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

垃圾回收相關概念

學習地址:https://www.bilibili.com/video/BV1PJ411n7xZ?p=154

System.gc()的理解

  1. 在預設情況下,通過system.gc()或者Runtime.getRuntime().gc()的呼叫,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件佔用的記憶體。

  2. 然而system.gc()呼叫附帶一個免責宣告,無法保證對垃圾收集器的呼叫。

  3. JVM實現者可以通過System.gc()呼叫來決定JVM的GC行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過於麻煩了。在一些特殊情況下,如我們正在編寫一個性能基準,我們可以在執行之間呼叫System.gc()。

關於免責宣告

public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc();//提醒jvm的垃圾回收器執行gc,但是不確定是否馬上執行gc
        //與Runtime.getRuntime().gc();的作用一樣。
        
        System.runFinalization();//強制呼叫使用引用的物件的finalize()方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重寫了finalize()");
    }
}

System.gc()演示理解

//-XX:+PrintGCDetails
public class LocalVarGC {
    public void localvarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];//10MB
        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();
    }

    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.localvarGC3();
    }
}
# localvarGC1() 沒有進行回收
[GC (System.gc()) [PSYoungGen: 15497K->10744K(76288K)] 15497K->10896K(251392K), 0.0068189 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 152K->10846K(175104K)] 10896K->10846K(251392K), [Metaspace: 3190K->3190K(1056768K)], 0.0110527 secs] [Times: user=0.01 sys=0.03, real=0.01 secs] 

# localvarGC2() 執行了回收 buffer = null,10MB沒有了引用
[GC (System.gc()) [PSYoungGen: 15497K->776K(76288K)] 15497K->784K(251392K), 0.0008100 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 776K->0K(76288K)] [ParOldGen: 8K->631K(175104K)] 784K->631K(251392K), [Metaspace: 3225K->3225K(1056768K)], 0.0059515 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

# localvarGC3() 沒有進行回收
[GC (System.gc()) [PSYoungGen: 15497K->10744K(76288K)] 15497K->10920K(251392K), 0.0084858 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 176K->10871K(175104K)] 10920K->10871K(251392K), [Metaspace: 3228K->3228K(1056768K)], 0.0131753 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 

# localvarGC4() 執行了回收
[GC (System.gc()) [PSYoungGen: 15497K->760K(76288K)] 15497K->768K(251392K), 0.0021767 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 760K->0K(76288K)] [ParOldGen: 8K->631K(175104K)] 768K->631K(251392K), [Metaspace: 3223K->3223K(1056768K)], 0.0051414 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

# localvarGC5() 執行了回收
[GC (System.gc()) [PSYoungGen: 15497K->10728K(76288K)] 15497K->10928K(251392K), 0.0078594 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 10728K->0K(76288K)] [ParOldGen: 200K->10871K(175104K)] 10928K->10871K(251392K), [Metaspace: 3225K->3225K(1056768K)], 0.0140686 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 

[GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] 10871K->10871K(251392K), 0.0004898 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 10871K->631K(175104K)] 10871K->631K(251392K), [Metaspace: 3225K->3225K(1056768K)], 0.0080775 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

針對localvarGC3()分析

此時buffer佔用著索引為一的位置所以沒有被回收

針對localvarGC4()分析

此時value佔用了索引為一的位置,buffer被回收

記憶體溢位與記憶體洩漏

記憶體溢位(OOM)

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

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

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

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

  5. 首先說沒有空閒記憶體的情況:說明〕ava虛擬機器的堆記憶體不夠。原因有二:

    • (1)Java虛擬機器的堆記憶體設定不夠。

      • 比如:可能存在記憶體洩漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的資料量,但是沒有顯式指定堆大小或者指定數值偏小:我們可以通過引數-Xms、-Xmx來調整;
    • (2)程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)
      對於老版本的Oracle JDK,因為永久代的大小是有限的,並且對永久代垃圾回收(如,常量池回收、解除安裝不再需要的型別)非常不積極,所以當我們不斷新增新型別的時候,永久代出現OutOdMemoryError也非常多見,尤其是在執行時存在大量動態型別生成的場合;類似intern字串快取佔用太多空間,也會導致00M問題。

      • 對應的異常資訊,會標記出來和永久代相關:"java.lang.OutOfMemoryError:PermGen space"

      • 隨著元資料區的引入,方法區記憶體己經不再那麼窘迫,所以相應的00M有所改觀,出現OOM,異常資訊則變成了:“java.lang.OutOfMemoryError:Metaspace"。直接記憶體不足,也會導致OOM。

  6. 這裡面隱含著一層意思是,在丟擲OutOfMemoryError之前,通常垃圾收集器會被觸發,盡其所能去清理出空間。

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

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

記憶體洩漏(Memory Leak)

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

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

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

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

舉例:

  1. 單例模式

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

    • 資料庫連線(dataSourse.getConnection()),網路連線(socket)和io連線必須手動close,否則是不能被回收的。

Stop The World

  1. Stop-the-world,簡稱STW,指的是GC事件發生過程中,會產生應用程式的停頓。停頓產生時整個應用程式執行緒都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。

    • 可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。

      • 分析工作必須在一個能確保一致性的快照中進行

      • 一致性指整個分析期間整個執行系績看起來像被凍結在某個時間點上

      • 如果出現分析過程中物件引用關係還在不斷變化,則分析結果的準確性無法保證

  2. 被STW中斷的應用程式執行緒會在完成GC之後恢復,頻繁中斷會讓使用者感覺像是網速不快造成電影卡帶一樣,所以我們需要減少STW的發生。

  3. STW事件和採用哪款GC無關,所有的GC都有這個事件。

  4. 哪怕是G1也不能完全避免Stop-the-world情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,儘可能地縮短了暫停時間。

  5. STW是JVM在後臺自動發起和自動完成的。在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉。

  6. 開發中不要用system.gc();會導致Stop-the-world的發生。

system.gc()導致Stop-the-world程式碼演示

public class StopTheWorldDemo {
    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();
            }
        }
    }

    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) {
        WorkThread w = new WorkThread();
        PrintThread p = new PrintThread();
        w.start();
        p.start();
    }
}
# 列印間隔不固定
0.8
1.9
2.25
3.28
4.33
5.38
6.38
7.52