最全的 JVM 面试知识点(二):垃圾收集
在上一篇介紹了 Java 虛擬機內存的運行時數據區。本章將會介紹 Java 中的垃圾收集算法與常用的垃圾收集器。
在涉及 Java 相關的面試中,面試官經常會讓講講 Java 中的垃圾收集相關的理解和常見的分類。可見,光就應付面試而言,JVM 的垃圾收集也對每一位 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 一個使用示例:
```javapublic static void main(String[] args) throws InterruptedException {SoftReference<String> sr = new SoftReference<>("hello");System.out.println(sr.get()); } ```弱引用
描述非必須的對象,Java 中常通過使用弱引用來避免內存泄漏。被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
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 垃圾收集中涉及的四種對象引用的類型:強引用、軟引用、弱引用和虛引用,對象死亡的判斷算法:引用計數法和可達性分析。最后介紹了幾種常見的垃圾收集算法。關于具體的垃圾回收器將在下一篇文章具體介紹。
訂閱最新文章,歡迎關注我的公眾號
總結
以上是生活随笔為你收集整理的最全的 JVM 面试知识点(二):垃圾收集的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北京圣思园_Java SE Lesson
- 下一篇: 东方幻想乡/BadApple!!