最全的 JVM 面試知識點(二):垃圾收集

上一篇介紹了 Java 虛擬機內存的運行時數據區。本章將會介紹 Java 中的垃圾收集算法與經常使用的垃圾收集器。html

在涉及 Java 相關的面試中,面試官常常會讓講講 Java 中的垃圾收集相關的理解和常見的分類。可見,光就應付面試而言,JVM 的垃圾收集也對每一位 Java 開發者很重要。除此以外,對於咱們瞭解和解決 Java 應用的性能時,也頗有幫助。java

本文的主要內容:面試

  • Java 中的引用
    • 強引用
    • 軟引用
    • 弱引用
    • 虛引用
  • 對象回收
    • 引用計數法
    • 可達性分析算法
  • 垃圾收集算法
    • 標記-清除算法
    • 標記-整理算法
    • 複製算法
    • 分代收集算法
  • 垃圾收集器
  • 小結

Java 中的引用

斷定對象是否存活都與引用有關。在Java語言中,將引用又分爲強引用、軟引用、弱引用和虛引用 4 種,這四種引用強度依次逐漸減弱。下面咱們一次介紹這四種引用。算法

強引用

在程序中廣泛存在的,相似 Object obj = new Object() 這類引用。只要強引用存在,垃圾收集器永遠不會回收掉被引用的對象。微信

軟引用

描述一些還有用但並不是必須的對象。在 Java 中用 java.lang.ref.SoftReference 類來表示。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收後尚未足夠的內存,纔會拋出內存溢出異常。SoftReference 類有兩個構造函數:多線程

public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }
    
    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }
複製代碼

軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關聯的引用隊列中。下面是 SoftReference 一個使用示例:併發

```java
    public static void main(String[] args) throws InterruptedException {
    SoftReference<String> sr = new SoftReference<>("hello");
    System.out.println(sr.get());
}
```
複製代碼

弱引用

描述非必須的對象,Java 中常經過使用弱引用來避免內存泄漏。被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。jvm

public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

複製代碼
public static void main(String[] args) throws InterruptedException {
    WeakReference<String> wr = new WeakReference<>("hello");

    System.out.println(wr.get());
    System.gc();                //通知JVM的gc進行垃圾回收
    Thread.sleep(1000);
    System.out.println(wr.get());
}
複製代碼

一樣,經過引用隊列這個參數,咱們便把建立的弱引用對象註冊到了一個引用隊列上,這樣當它被垃圾回收器清除時,就會把它送入這個引用隊列中,咱們即可以對這些被清除的弱引用對象進行統一管理。函數

軟引用和弱引用的區別在於,若一個對象是弱引用可達,不管當前內存是否充足它都會被回收,而軟引用可達的對象在內存不充足時纔會被回收,所以軟引用要比弱引用強一些。性能

虛引用

幽靈引用或者幻影引用。最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。它的做用是能在這個對象被收集器回收時收到一個系統通知。

public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        PhantomReference<String> pr = new PhantomReference<>("hello", queue);
        System.out.println(pr.get());
    }
複製代碼

要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。若是程序發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。

對象回收

堆中幾乎放着全部的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。

引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任什麼時候候計數器爲 0 的對象就是不可能再被使用的。

引用計數法能夠很快的執行,交織在程序運行中。對程序須要不被長時間打斷的實時環境比較有利。實現簡單,效率高,可是目前主流的虛擬機中並無選擇這個算法來管理內存,其最主要的緣由是它很難解決對象之間相互循環引用的問題。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲 0。

可達性分析算法

這個算法的基本思想就是經過一系列的稱爲 GC Roots 的對象做爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證實此對象是不可用的。

在 Java 中哪些對象能夠成爲 GC Root?

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

垃圾收集算法

經常使用的垃圾收集算法有:標記-清除、標記-整理、複製和分代收集算法。下面依次介紹這幾種垃圾收集算法。

標記-清除算法

首先標記出須要回收的對象,在標記完成後統一回收掉全部的被標記對象。

缺點:效率問題和空間問題(標記清除後會產生大量的不連續內存碎片,內存碎片過多可能會致使程序須要分配較大對象時找不到足夠大的連續內存空間而不得不提早觸發另外一次垃圾回收動做)

標記-整理算法

標記-整理 算法採用 標記-清除 算法同樣的方式進行對象的標記,但在清除時不一樣,在回收不存活的對象佔用的空間後,會將全部的存活對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,所以成本更高,可是卻解決了內存碎片的問題。

複製算法

將內存劃分爲大小相等的兩塊,每次只使用其中的一塊。當這塊內存用完了,就將還存活的對象複製到另外一塊內存上,而後把已使用過的內存空間一次清理掉。

複製算法的提出是爲了克服句柄的開銷和解決內存碎片的問題。每次只對其中一塊進行GC,不用考慮內存碎片的問題,而且實現簡單,運行高效。缺點是內存縮小了一半。

分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區以外還有一個代就是永久代(Permanet Generation)。老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

年輕代(Young Generation)的回收算法

全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。

新生代內存按照 8:1:1 的比例分爲一個 Eden 區和兩個 survivor(survivor0,survivor1) 區。一個E den 區,兩個 Survivor 區(通常而言)。大部分對象在 Eden 區中生成。回收時先將 Eden 區存活對象複製到一個 survivor0 區,而後清空 Eden 區,當這個 survivor0 區也存放滿了時,則將 Eden 區和 survivor0 區存活對象複製到另外一個 survivor1 區,而後清空 Eden 和這個 survivor0 區,此時 survivor0 區是空的,而後將 survivor0 區和 survivor1 區交換,即保持 survivor1 區爲空, 如此往復。

當 survivor1 區不足以存放 Eden 和 survivor0 的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次 Full GC,也就是新生代、老年代都進行回收。

新生代發生的 GC 也叫作 Minor GC,Minor GC 發生頻率比較高(不必定等 Eden 區滿了才觸發)。

年老代(Old Generation)的回收算法

在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

內存比新生代也大不少(大概比例是 1:2),當老年代內存滿時觸發 Major GC 即 Full GC,Full GC 發生頻率比較低,老年代對象存活時間比較長,存活率標記高。

持久代(Permanent Generation)的回收算法

用於存放靜態文件,如 Java 類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些 class,例如 Hibernate 等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。持久代也稱方法區,方法區存儲內容是否須要回收的判斷不同。方法區主要回收的內容有:廢棄常量和無用的類。對於廢棄常量也可經過引用的可達性來判斷,可是對於無用的類則須要同時知足下面 3 個條件:

  • 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例;
  • 加載該類的 ClassLoader 已經被回收;
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

垃圾收集器

不一樣的垃圾回收器,適用於不一樣的場景。經常使用的垃圾回收器:

  • 串行(Serial)回收器是單線程的一個回收器,簡單、易實現、效率高。
  • 並行(ParNew)回收器是Serial的多線程版,能夠充分的利用CPU資源,減小回收的時間。
  • 吞吐量優先(Parallel Scavenge)回收器,側重於吞吐量的控制。
  • 併發標記清除(CMS,Concurrent Mark Sweep)回收器是一種以獲取最短回收停頓時間爲目標的回收器,該回收器是基於 標記-清除 算法實現的。

小結

本文講了 JVM 垃圾收集中涉及的四種對象引用的類型:強引用、軟引用、弱引用和虛引用,對象死亡的判斷算法:引用計數法和可達性分析。最後介紹了幾種常見的垃圾收集算法。關於具體的垃圾回收器將在下一篇文章具體介紹。

訂閱最新文章,歡迎關注個人公衆號

微信公衆號

參考
  1. 扒一扒JVM的垃圾回收機制,下次面試你準備好了嗎
  2. Java 如何有效地避免OOM:善於利用軟引用和弱引用
相關文章
相關標籤/搜索