天天看點

垃圾回收算法JVM 垃圾回收

點選關注公衆号及時擷取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。

  • JVM 垃圾回收
    • 寫在前面
      • 本節常見面試題
      • 本文導火索
    • 1 揭開 JVM 記憶體配置設定與回收的神秘面紗
      • 1.1 對象優先在 eden 區配置設定
      • 1.2 大對象直接進入老年代
      • 1.3 長期存活的對象将進入老年代
      • 1.4 動态對象年齡判定
    • 2 對象已經死亡?
      • 2.1 引用計數法
      • 2.2 可達性分析算法
      • 2.3 再談引用
      • 2.4 不可達的對象并非“非死不可”
      • 2.5 如何判斷一個常量是廢棄常量
      • 2.6 如何判斷一個類是無用的類
    • 3 垃圾收集算法
      • 3.1 标記-清除算法
      • 3.2 複制算法
      • 3.3 标記-整理算法
      • 3.4 分代收集算法
    • 4 垃圾收集器
      • 4.1 Serial 收集器
      • 4.2 ParNew 收集器
      • 4.3 Parallel Scavenge 收集器
      • 4.4.Serial Old 收集器
      • 4.5 Parallel Old 收集器
      • 4.6 CMS 收集器
      • 4.7 G1 收集器
    • 參考

JVM 垃圾回收

寫在前面

本節常見面試題

問題答案在文中都有提到

  • 如何判斷對象是否死亡(兩種方法)。
  • 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的差別、使用軟引用能帶來的好處)。
  • 如何判斷一個常量是廢棄常量
  • 如何判斷一個類是無用的類
  • 垃圾收集有哪些算法,各自的特點?
  • HotSpot 為什麼要分為新生代和老年代?
  • 常見的垃圾回收器有那些?
  • 介紹一下 CMS,G1 收集器。
  • Minor Gc 和 Full GC 有什麼不同呢?

本文導火索

垃圾回收算法JVM 垃圾回收

當需要排查各種記憶體溢出問題、當垃圾收內建為系統達到更高并發的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。

1 揭開 JVM 記憶體配置設定與回收的神秘面紗

Java 的自動記憶體管理主要是針對對象記憶體的回收和對象記憶體的配置設定。同時,Java 自動記憶體管理最核心的功能是 堆 記憶體中對象的配置設定與回收。

Java 堆是垃圾收集器管理的主要區域,是以也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,是以 Java 堆還可以細分為:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收記憶體,或者更快地配置設定記憶體。

堆空間的基本結構:

垃圾回收算法JVM 垃圾回收

上圖所示的 eden 區、s0(“From”) 區、s1(“To”) 區都屬于新生代,tentired 區屬于老年代。大部分情況,對象都會首先在 Eden 區域配置設定,在一次新生代垃圾回收後,如果對象還存活,則會進入 s1(“To”),并且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變為 1),當它的年齡增加到一定程度(預設為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡門檻值,可以通過參數

-XX:MaxTenuringThreshold

來設定。經過這次GC後,Eden區和"From"區已經被清空。這個時候,“From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,"To"區被填滿之後,會将所有對象移動到年老代中。

垃圾回收算法JVM 垃圾回收

1.1 對象優先在 eden 區配置設定

目前主流的垃圾收集器都會采用分代回收算法,是以需要将堆記憶體分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合适的垃圾收集算法。

大多數情況下,對象在新生代中 eden 區配置設定。當 eden 區沒有足夠空間進行配置設定時,虛拟機将發起一次 Minor GC.下面我們來進行實際測試以下。

在測試之前我們先來看看 Minor GC 和 Full GC 有什麼不同呢?

  • 新生代 GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC 非常頻繁,回收速度一般也比較快。
  • 老年代 GC(Major GC/Full GC):指發生在老年代的 GC,出現了 Major GC 經常會伴随至少一次的 Minor GC(并非絕對),Major GC 的速度一般會比 Minor GC 的慢 10 倍以上。

測試:

public class GCTest {

	public static void main(String[] args) {
		byte[] allocation1, allocation2;
		allocation1 = new byte[30900*1024];
		//allocation2 = new byte[900*1024];
	}
}
           

通過以下方式運作:

垃圾回收算法JVM 垃圾回收

添加的參數:

-XX:+PrintGCDetails

垃圾回收算法JVM 垃圾回收

運作結果 (紅色字型描述有誤,應該是對應于 JDK1.7 的永久代):

垃圾回收算法JVM 垃圾回收

從上圖我們可以看出 eden 區記憶體幾乎已經被配置設定完全(即使程式什麼也不做,新生代也會使用 2000 多 k 記憶體)。假如我們再為 allocation2 配置設定記憶體會出現什麼情況呢?

垃圾回收算法JVM 垃圾回收

簡單解釋一下為什麼會出現這種情況: 因為給 allocation2 配置設定記憶體的時候 eden 區記憶體幾乎已經被配置設定完了,我們剛剛講了當 Eden 區沒有足夠空間進行配置設定時,虛拟機将發起一次 Minor GC.GC 期間虛拟機又發現 allocation1 無法存入 Survivor 空間,是以隻好通過 配置設定擔保機制 把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放 allocation1,是以不會出現 Full GC。執行 Minor GC 後,後面配置設定的對象如果能夠存在 eden 區的話,還是會在 eden 區配置設定記憶體。可以執行如下代碼驗證:

public class GCTest {

	public static void main(String[] args) {
		byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
		allocation1 = new byte[32000*1024];
		allocation2 = new byte[1000*1024];
		allocation3 = new byte[1000*1024];
		allocation4 = new byte[1000*1024];
		allocation5 = new byte[1000*1024];
	}
}

           

1.2 大對象直接進入老年代

大對象就是需要大量連續記憶體空間的對象(比如:字元串、數組)。

為什麼要這樣呢?

為了避免為大對象配置設定記憶體時由于配置設定擔保機制帶來的複制而降低效率。

1.3 長期存活的對象将進入老年代

既然虛拟機采用了分代收集的思想來管理記憶體,那麼記憶體回收時就必須能識别哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛拟機給每個對象一個對象年齡(Age)計數器。

如果對象在 Eden 出生并經過第一次 Minor GC 後仍然能夠存活,并且能被 Survivor 容納的話,将被移動到 Survivor 空間中,并将對象年齡設為 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(預設為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡門檻值,可以通過參數

-XX:MaxTenuringThreshold

來設定。

1.4 動态對象年齡判定

為了更好的适應不同程式的記憶體情況,虛拟機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果 Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需達到要求的年齡。

2 對象已經死亡?

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

垃圾回收算法JVM 垃圾回收

2.1 引用計數法

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

這個方法實作簡單,效率高,但是目前主流的虛拟機中并沒有選擇這個算法來管理記憶體,其最主要的原因是它很難解決對象之間互相循環引用的問題。 所謂對象之間的互相引用問題,如下面代碼所示:除了對象 objA 和 objB 互相引用着對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導緻它們的引用計數器都不為 0,于是引用計數算法無法通知 GC 回收器回收他們。

public class ReferenceCountingGc {
    Object instance = null;
	public static void main(String[] args) {
		ReferenceCountingGc objA = new ReferenceCountingGc();
		ReferenceCountingGc objB = new ReferenceCountingGc();
		objA.instance = objB;
		objB.instance = objA;
		objA = null;
		objB = null;

	}
}
           

2.2 可達性分析算法

這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜尋,節點所走過的路徑稱為引用鍊,當一個對象到 GC Roots 沒有任何引用鍊相連的話,則證明此對象是不可用的。

垃圾回收算法JVM 垃圾回收

2.3 再談引用

無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鍊是否可達,判定對象的存活都與“引用”有關。

JDK1.2 之前,Java 中引用的定義很傳統:如果 reference 類型的資料存儲的數值代表的是另一塊記憶體的起始位址,就稱這塊記憶體代表一個引用。

JDK1.2 以後,Java 對引用的概念進行了擴充,将引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

1.強引用(StrongReference)

以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空間不足,Java 虛拟機甯願抛出 OutOfMemoryError 錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足問題。

2.軟引用(SoftReference)

如果一個對象隻具有軟引用,那就類似于可有可無的生活用品。如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些對象的記憶體。隻要垃圾回收器沒有回收它,該對象就可以被程式使用。軟引用可用來實作記憶體敏感的高速緩存。

軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛拟機就會把這個軟引用加入到與之關聯的引用隊列中。

3.弱引用(WeakReference)

如果一個對象隻具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的差別在于:隻具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。不過,由于垃圾回收器是一個優先級很低的線程, 是以不一定會很快發現那些隻具有弱引用的對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛拟機就會把這個弱引用加入到與之關聯的引用隊列中。

4.虛引用(PhantomReference)

"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

虛引用主要用來跟蹤對象被垃圾回收的活動。

虛引用與軟引用和弱引用的一個差別在于: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中。程式可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否将要被垃圾回收。程式如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前采取必要的行動。

特别注意,在程式設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速 JVM 對垃圾記憶體的回收速度,可以維護系統的運作安全,防止記憶體溢出(OutOfMemory)等問題的産生。

2.4 不可達的對象并非“非死不可”

即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經曆兩次标記過程;可達性分析法中不可達的對象被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆寫 finalize 方法,或 finalize 方法已經被虛拟機調用過時,虛拟機将這兩種情況視為沒有必要執行。

被判定為需要執行的對象将會被放在一個隊列中進行第二次标記,除非這個對象與引用鍊上的任何一個對象建立關聯,否則就會被真的回收。

2.5 如何判斷一個常量是廢棄常量

運作時常量池主要回收的是廢棄的常量。那麼,我們如何判斷一個常量是廢棄常量呢?

假如在常量池中存在字元串 “abc”,如果目前沒有任何 String 對象引用該字元串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發生記憶體回收的話而且有必要的話,“abc” 就會被系統清理出常量池。

注意:我們在 2.6 如何判斷一個類是無用的類

方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 “無用的類” :

  • 該類所有的執行個體都已經被回收,也就是 Java 堆中不存在該類的任何執行個體。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

虛拟機可以對滿足上述 3 個條件的無用類進行回收,這裡說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。

3 垃圾收集算法

垃圾回收算法JVM 垃圾回收

3.1 标記-清除算法

該算法分為“标記”和“清除”階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。它是最基礎的收集算法,後續的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:

  1. 效率問題
  2. 空間問題(标記清除後會産生大量不連續的碎片)
垃圾回收算法JVM 垃圾回收

3.2 複制算法

為了解決效率問題,“複制”收集算法出現了。它可以将記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就将還存活的對象複制到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的記憶體回收都是對記憶體區間的一半進行回收。

垃圾回收算法JVM 垃圾回收

3.3 标記-整理算法

根據老年代的特點提出的一種标記算法,标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的記憶體。

垃圾回收算法JVM 垃圾回收

3.4 分代收集算法

目前虛拟機的垃圾收集都采用分代收集算法,這種算法沒有什麼新的思想,隻是根據對象存活周期的不同将記憶體分為幾塊。一般将 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合适的垃圾收集算法。

比如在新生代中,每次收集都會有大量對象死去,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行配置設定擔保,是以我們必須選擇“标記-清除”或“标記-整理”算法進行垃圾收集。

延伸面試問題: HotSpot 為什麼要分為新生代和老年代?

根據上面的對分代收集算法的介紹回答。

4 垃圾收集器

垃圾回收算法JVM 垃圾回收

如果說收集算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實作。

雖然我們對各個收集器進行比較,但并非要挑選出一個最好的收集器。因為直到現在為止還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇适合自己的垃圾收集器。試想一下:如果有一種四海之内、任何場景下都适用的完美收集器存在,那麼我們的 HotSpot 虛拟機就不會實作那麼多不同的垃圾收集器了。

4.1 Serial 收集器

Serial(串行)收集器收集器是最基本、曆史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它隻會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。

新生代采用複制算法,老年代采用标記-整理算法。

垃圾回收算法JVM 垃圾回收

虛拟機的設計者們當然知道 Stop The World 帶來的不良使用者體驗,是以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是 Serial 收集器有沒有優于其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由于沒有線程互動的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對于運作在 Client 模式下的虛拟機來說是個不錯的選擇。

4.2 ParNew 收集器

ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為(控制參數、收集算法、回收政策等等)和 Serial 收集器完全一樣。

新生代采用複制算法,老年代采用标記-整理算法。

垃圾回收算法JVM 垃圾回收

它是許多運作在 Server 模式下的虛拟機的首要選擇,除了 Serial 收集器外,隻有它能與 CMS 收集器(真正意義上的并發收集器,後面會介紹到)配合工作。

并行和并發概念補充:

  • 并行(Parallel) :指多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态。
  • 并發(Concurrent):指使用者線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),使用者程式在繼續運作,而垃圾收集器運作在另一個 CPU 上。

4.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器也是使用複制算法的多線程收集器,它看上去幾乎和ParNew都一樣。 那麼它有什麼特别之處呢?

-XX:+UseParallelGC 

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代并行

           

Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是使用者線程的停頓時間(提高使用者體驗)。所謂吞吐量就是 CPU 中用于運作使用者代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了很多參數供使用者找到最合适的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,手工優化存在困難的話可以選擇把記憶體管理優化交給虛拟機去完成也是一個不錯的選擇。

新生代采用複制算法,老年代采用标記-整理算法。

垃圾回收算法JVM 垃圾回收

4.4.Serial Old 收集器

Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的後備方案。

4.5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多線程和“标記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。

4.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。它非常符合在注重使用者體驗的應用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛拟機第一款真正意義上的并發收集器,它第一次實作了讓垃圾收集線程與使用者線程(基本上)同時工作。

從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “标記-清除”算法實作的,它的運作過程相比于前面幾種垃圾收集器來說更加複雜一些。整個過程分為四個步驟:

  • 初始标記: 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ;
  • 并發标記: 同時開啟 GC 和使用者線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含目前所有的可達對象。因為使用者線程可能會不斷的更新引用域,是以 GC 線程無法保證可達性分析的實時性。是以這個算法裡會跟蹤記錄這些發生引用更新的地方。
  • 重新标記: 重新标記階段就是為了修正并發标記期間因為使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段的時間稍長,遠遠比并發标記階段時間短
  • 并發清除: 開啟使用者線程,同時 GC 線程開始對為标記的區域做清掃。
垃圾回收算法JVM 垃圾回收

從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:并發收集、低停頓。但是它有下面三個明顯的缺點:

  • 對 CPU 資源敏感;
  • 無法處理浮動垃圾;
  • 它使用的回收算法-“标記-清除”算法會導緻收集結束時會有大量空間碎片産生。

4.7 G1 收集器

G1 (Garbage-First) 是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高機率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.

被視為 JDK1.7 中 HotSpot 虛拟機的一個重要進化特征。它具備一下特點:

  • 并行與并發:G1 能充分利用 CPU、多核環境下的硬體優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過并發的方式讓 java 程式繼續執行。
  • 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
  • 空間整合:與 CMS 的“标記–清理”算法不同,G1 從整體來看是基于“标記整理”算法實作的收集器;從局部上來看是基于“複制”算法實作的。
  • 可預測的停頓:這是 G1 相對于 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為 M 毫秒的時間片段内。

G1 收集器的運作大緻分為以下幾個步驟:

  • 初始标記
  • 并發标記
  • 最終标記
  • 篩選回收

G1 收集器在背景維護了一個優先清單,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)。這種使用 Region 劃分記憶體空間以及有優先級的區域回收方式,保證了 GF 收集器在有限時間内可以盡可能高的收集效率(把記憶體化整為零)。

參考

  • 《深入了解 Java 虛拟機:JVM 進階特性與最佳實踐(第二版》
  • https://my.oschina.net/hosee/blog/644618
  • https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

公衆号

如果大家想要實時關注我更新的文章以及分享的幹貨的話,可以關注我的公衆号。

《Java面試突擊》: 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本公衆号背景回複 “Java面試突擊” 即可免費領取!

Java工程師必備學習資源: 一些Java工程師常用學習資源公衆号背景回複關鍵字 “1” 即可免費無套路擷取。

垃圾回收算法JVM 垃圾回收

繼續閱讀