1. 程式人生 > 其它 >JVM垃圾回收面試題詳解,Java入門程式設計教程

JVM垃圾回收面試題詳解,Java入門程式設計教程

JVM垃圾回收面試題詳解,Java入門程式設計教程

2.2.2 哪些物件可作為GC Roots

有四類物件可作為可達性分析的GC Roots

  • 棧(棧幀中的本地變量表)中引用的物件

  • 方法區中類靜態屬性引用的物件

  • 方法區中常量引用的物件

  • 本地方法棧中JNI引用的物件

總而言之,GC Roots是所有Java執行緒中處於活躍狀態的棧幀,靜態引用等指向GC堆裡的物件的引用。換句話說,就是當前所有正在被呼叫的方法的引用型別的引數/區域性變數/臨時值。

2.3 所謂引用

物件是否死亡,關鍵就在於引用。在java中,引用其實有四種:強引用、軟引用、弱引用、虛引用。

  • 強引用

    強引用就是我們日常開發中最常見的引用,例如

    
    String str = new String("hello");
    
    

    只要強引用還在,物件就不會被回收。

  • 軟引用

    軟引用需要專門宣告,例如

    
    SoftReference<String> str = new SoftReference<String>("hello");
    
    

    被軟引用關聯的物件在記憶體不足時會被回收

    這個特性特別適合用來做快取。

  • 弱引用

    弱引用也需要專門宣告,例如

    
    WeakReference<String> str = new WeakReference<String>("hello");
    
    

    被弱引用關聯的物件每次GC時都會被回收

    弱引用最常見的用途是實現可自動清理的集合或者佇列。

  • 虛引用

    虛引用是最弱的引用,需要用PhantomReference來宣告,例如

    
    PhantomReference<String> phantom = new PhantomReference<>(new String("hello"), new ReferenceQueue<>());
    
    

    它完全不會影響物件的生存時間,唯一的作用是在物件被回收時發一個系統通知。

2.4 起死回生

物件在被判定為死亡後,並不會立刻被回收,而是要經過一個過程才會被回收。在這個回收過程中,死亡物件還有可能活過來,是不是很神奇?

來看圖:

上圖是物件被回收的過程。一個物件要被回收,至少要經過兩次標記

如果物件在第二次標記之前重新連線上GC Roots,那麼它將在第二次標記中被移出回收佇列,從而復活。

還有一點需要注意的是,Finalizer執行緒是一個由虛擬機器自動建立,且低優先順序的執行緒。該執行緒觸發物件的finalize()方法之後,並不會阻塞等待方法執行結束。這樣做是為了防止回收佇列被阻塞。

finalize()是Object中的方法,當垃圾回收器將要回收物件所佔記憶體之前被呼叫的方法。有些教材推薦用該方法來做“關閉外部資源”之類的工作,但是實際上該方法執行代價高昂,且不確定性很大,所以並不推薦使用。真要關閉外部資源,還不如用try-finally來處理。

3.方法區的回收


方法區不在堆內,會被垃圾回收嗎?

在jdk1.7中,方法區在永久代,而永久代本身就是垃圾回收概念下的產物,full gc時就會對方法區回收。

到了jdk1.8,雖然永久代被取消,但是新增了MaxMetaspaceSize引數,對於將死的類及類載入器的垃圾回收將在元資料使用達到“MaxMetaspaceSize”引數的設定值時進行。

所以,方法區會被回收。

4.垃圾回收演算法


這一節我們來看下流行的垃圾回收演算法,只說思想,不涉及實現細節。

我們需要了解的垃圾回收演算法有以下幾種:

  • 標記-清除演算法

  • 複製演算法

  • 標記-整理演算法

  • 分代回收演算法

咱們一個個來看下。

4.1 標記-清除演算法

標記-清除算是最基本的回收演算法了。它的思想就是先標記,再清除。標記過程如2.4節所述,有兩次標記。

它的主要缺點有兩個:

  • 效率不高

  • 會產生大量記憶體碎片

記憶體碎片是指記憶體的空間比較零碎,缺少大段的連續空間。這樣假如突然來了一個大物件,會找不到足夠大的連續空間來存放,於是不得不再觸發一次gc。

4.2 複製演算法

複製演算法的思想是,把記憶體分成兩塊,假設分成A、B兩個區域吧。

每次物件過來之後,都放到A區域裡,當A區域滿了之後,把存活的物件複製到B區域,然後清空A區域。

接下來的物件就全部放到B區域,等B區域滿了,就把存活物件複製到A區域,然後清空B區域。

就這樣來回倒騰,完成垃圾回收。

優點是不會有空間碎片缺點是每次只用得到一半記憶體

缺點是在物件存活率較高的場景下(比如老年代那樣的環境),需要複製的東西太多,效率會下降

4.3 標記-整理演算法

標記-整理演算法中的“標記”階段和“標記-清理”中的標記一樣。不同的是,死亡物件並不會直接清理,而是把他們在記憶體中都移動到一起,然後一起清理。

4.4 分代收集演算法

分代收集演算法其實沒什麼新東西,只是把物件按存活率分塊,然後選用合適的收集演算法。

java中使用的就是分代收集演算法。

存活率低的物件放在一起,稱為年輕代,使用複製演算法來收集。

存活率高的物件放在一起,稱為老年代,使用標記-清除或者標記-整理演算法。

5. HotSpot的列舉GC Roots


前面我們說到了物件的可達性分析需要從GC Roots開始計算引用鏈。

然而可作為GC Roots的物件非常多,一個個來計算將非常耗時。

而且在進行這項工作時,虛擬機器必須停下來,就像時間停止那樣(Sun稱之為Stop The World,哈哈,是不是很酷),以此保證分析結果的準確性。

我們的程式,特別是網站應用,基本是上是一刻不停的在執行的。如果出現長時間的停止,基本上是不可接受的。為了解決這個問題,各個虛擬機器都採取了一些措施,儘量減少停頓時間(是的,只能減少,停頓是不可能消除的)。

我們來看看現在最流行的Hotspot虛擬機器是怎麼處理的。(還記得啥是HotSpot不?翻翻前幾篇文章)

5.1 OopMap

在HotSpot中,虛擬機器把物件內的什麼偏移量上是什麼型別的資料的資訊存在到一個叫做“OopMap”的資料結構中。這樣在計算引用鏈時直接查OopMap即可,不用到整個記憶體中去挨個找了,由此提高了分析速度。

5.2 安全點

然而,程式中的引用關係時時刻刻都在變化,如果每次變化都要記錄到OopMap中,也是一項很大的負擔。所以,只有在程式執行到了特定的位置,才會去記錄到OopMap中。

這個“特定的位置”,就叫安全點

這裡面還有個問題,就是如何保證在GC發生時,讓所有的執行緒正好到達安全點。

有兩種方式:

  • 搶先式中斷(已經沒人用了)

    搶先式中斷的思路是,先把所有執行緒中斷,如果有執行緒沒有跑到安全點上,就恢復該執行緒,讓它跑到安全點。

  • 主動式中斷

    主動式中斷的做法是,設定一箇中斷標誌,這個標誌和安全點是重合的。讓各個執行緒去輪詢這個標誌,發現需要中斷時,執行緒就自己中斷掛起。

5.3 安全區域

雖然安全點已經完美解決了如何保證在GC發生時,讓所有的執行緒正好到達安全點的問題。

但是有一些情況下,執行緒失去了行為能力,比如執行緒處於sleep或者blocked狀態。這個時候執行緒無法去響應JVM的中斷請求,而JVM顯然也不肯能一直等待某幾個執行緒。該怎麼辦呢?

這種情況就需要“安全區域”來解決。

安全區域是指在一段程式碼片段中,引用關係不會發生變化,這個區域中任意地方開始GC都是安全的。

6.垃圾收集器


前面咱們說的都是垃圾收集的方法和思路,垃圾收集器則是具體的實現。

先來看下hotSpot中垃圾收集器的總圖(到jdk1.8)

6.1 並行和併發

在開始講解之前,我們先了解一下什麼是並行和併發。

並行:垃圾收集器是多執行緒同時工作的,但是使用者執行緒仍然處於等待狀態。

併發:使用者執行緒和垃圾收集器執行緒同時執行(也有可能是交替執行)。

下面咱們說說幾個常用的使用方案

6.1 jdk1.8預設垃圾收集器

檢視當前使用的垃圾收集器可以使用以下命令:


~ java -XX:+PrintCommandLineFlags -version

然後會看到以下內容:


-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC

java version "1.8.0_151"

Java(TM) SE Runtime Environment (build 1.8.0_151-b12)

Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

可見jdk1.8預設工作在Server模式下,預設使用ParallelGC垃圾收集器

如果要看更詳細的資訊,還可以使用以下命令:


java -XX:+PrintFlagsFinal -version | grep GC

這個命令列印的內容有點多,我們主要找值為true的資訊。預設情況會有以下兩行:


bool UseParallelGC                            := true    

bool UseParallelOldGC                          = true 

6.1.1 Parallel Scavenge收集器

從上面的總圖能看到,這是一個工作在年輕代的收集器,使用複製演算法,是一個並行的多執行緒收集器。

它的目標是達到一個可控制的吞吐量。所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值。比如虛擬機器總共運行了100分鐘,其中垃圾收集花了1分鐘,那吞吐量就是99%。

6.1.2 Parallel Old收集器

Parallel Old是一個工作在老年代的收集器,使用“標記-整理”演算法。也是一個關注吞吐量的垃圾收集器。

6.2 web應用垃圾收集器方案

ParallelGC組合重視的是吞吐量,非常適合在後臺運算而不需要太多互動的場景。

對於需要大量互動的應用,比如web應用,則需要更短的停頓時間。

所以大多數web應用使用的是ParNew+CMS收集器方案。

6.2.1 ParNew收集器

parNew也是一個工作在年輕代的收集器,也使用複製演算法,也是一個並行的多執行緒收集器。

為什麼我要使用這麼多“也”……

好吧,parNew看起來和Parallel Scavenge一模一樣,但其實他們還是有區別的。

parNew是一個重視停頓時間收集器。

不過它最大的特點是:可以和CMS收集器組隊工作。

Parallel Scavenge就不行…...

6.2.2 CMS收集器

CMS是一款十分優秀的老年代垃圾收集器,響應速度快、停頓時間短,是現在大多數網際網路公司的選擇,大家要好好掌握。

CMS使用“標記-清除”演算法,分為4個步驟:

寫在最後

學習技術是一條慢長而艱苦的道路,不能靠一時激情,也不是熬幾天幾夜就能學好的,必須養成平時努力學習的習慣。所以:貴在堅持!

最後再分享的一些BATJ等大廠20、21年的面試題,把這些技術點整理成了視訊和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節,由於篇幅有限,上面只是以圖片的形式給大家展示一部分。

領取方式:戳這裡即可免費領取

Mybatis面試專題

MySQL面試專題

併發程式設計面試專題