淺談垃圾回收

引言

一直都想寫一篇博客結合前輩的經驗去闡述自己對垃圾回收的理解,但阿導本人經驗尚欠,怕寫的不好,所以拖了很久很久,文章中不足之處,還請多多包涵。

垃圾回收的背景

垃圾回收(Garbage collection),簡稱 GC,很多人都認爲它是伴隨 JAVA 的衍生物,其實不然,1960年Lisp這門語言中就使用了內存動態分配和垃圾回收技術,GC 比 JAVA (1995年誕生)要久遠的多。

垃圾回收的策略

JVM 的內存結構包括五大區域:程序計數器、虛擬機棧、本地方法棧、堆區、方法區。
其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生、隨線程而滅,因此這幾個區域的內存分配和回收都具備確定性,就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。而 Java 堆區和方法區則不一樣,這部分內存的分配和回收是動態的,正是垃圾收集器所需關注的部分。
我們常見的垃圾回收策略有如下兩種,下面允許我簡要介紹一波。

引用計數法

這種垃圾回收策略是比較早的一種策略,這種回收策略就是堆中對象實例有一個引用計數,當對象被創建,該引用計數便會加 1,但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。

下面分析一下這種垃圾回收的優缺點

  • 優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。
  • 缺點:無法檢測到循環引用,因爲他們引用計數永遠不會爲0導致其無法進行垃圾回收。

引用計數法

可達性分析法

可達性分析法來源於離散數學的圖論,程序會把引用關係看做一張圖,從 GC Root 節點出發,向下找出所有被引用的節點,那些沒有找到的節點就會被當成垃圾進行回收。
一般在 java 中可以作爲 GC Root 的對象如下:

  • 虛擬機棧中引用的對象(棧幀中的本地變量表)
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中 JNI(Native方法)引用的對象
    可達性分析法
    在可達性分析法中不可達的對象,也不一定會被回收,這時候它們暫時處於第一次標記階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程。
  • 第一次標記:如果對象在進行可達性分析後發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記;
  • 第二次標記:第一次標記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法。在 finalize() 方法中沒有重新與引用鏈建立關聯關係的,將被進行第二次標記。
  • 第二次標記成功的對象將真的會被回收,如果對象在 finalize() 方法中重新與引用鏈建立了關聯關係,那麼將會逃離本次回收,繼續存活。

方法區存儲內容是否需要回收的判斷可就不一樣了。方法區主要回收的內容有:廢棄常量和無用的類。

  • 對於廢棄常量也可通過引用的可達性來判斷,

  • 對於無用的類則需要同時滿足下面3個條件:

      該類所有的實例都已經被回收,即 Java 堆中不存在該類的任何實例
      加載該類的 ClassLoader 已經被回收
      該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
    

常見的垃圾回收算法

標記-清除算法(Mark-Sweep)

標記-清除算法採用從根集合(GC Roots)進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收,如下圖所示。標記-清除算法不需要進行對象的移動,只需對不存活的對象進行處理,在存活對象比較多的情況下極爲高效,但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片。

標記清除算法

複製算法(Copying)

複製算法的提出是爲了克服句柄的開銷和解決內存碎片的問題。它開始時把堆分成 一個對象 面和多個空閒面, 程序從對象面爲對象分配空間,當對象滿了,基於 copying 算法的垃圾 收集就從根集合(GC Roots)中掃描活動對象,並將每個 活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。
複製算法

標記整理算法(Mark-Compact)

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

分代收集算法

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

  • 年輕代(Young Generation):主要以複製算法進行垃圾回收
  1. 年輕代存放所有新生成的對象。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。

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

  3. 當 Survivor1 區不足以存放 Eden 和 Survivor0 的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次 Full GC(Major GC),也就是新生代、老年代都進行回收。

  4. 新生代發生的 GC 也叫做 Minor GC,MinorGC 發生頻率比較高,但不一定等 Eden 區滿了才觸發。

  • 年老代(Tenured Generation):主要以標記-整理算法進行垃圾回收
  1. 在年輕代中經歷了 N 次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。

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

  • 永久代(Permanet Generation)

用於存放靜態文件,如 Java 類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些 class,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代也稱方法區。

常見的垃圾回收器對比

垃圾回收器 回收器使用的算法 回收器簡介
Serial 複製算法 新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。是 client 級別默認的 GC 方式,可以通過 -XX:+UseSerialGC 來強制指定。
Serial Old 標記-整理算法 老年代單線程收集器,Serial 收集器的老年代版本。
ParNew 停止-複製算法 新生代收集器,可以認爲是 Serial 收集器的多線程版本,在多核 CPU 環境下有着比 Serial 更好的表現
Parallel Scavenge 停止-複製算法 並行收集器,追求高吞吐量,高效利用 CPU。吞吐量一般爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC 線程時間)。適合後臺應用等對交互相應要求不高的場景。是 server 級別默認採用的 GC 方式,可用 -XX:+UseParallelGC 來強制指定,用-XX:ParallelGCThreads=4 來指定線程數。
Parallel Old 停止-複製算法 Parallel Scavenge 收集器的老年代版本,並行收集器,吞吐量優先。
CMS(Concurrent Mark Sweep) 標記-清理算法 高併發、低停頓,追求最短 GC 回收停頓時間,cpu 佔用比較高,響應時間快,停頓時間短,多核 cpu 追求高響應時間的選擇。
G1 Garbage-First(G1,垃圾優先)收集器是服務類型的收集器,目標是多處理器機器、大內存機器。它高度符合垃圾收集暫停時間的目標,同時實現高吞吐量。Oracle JDK 7 update 4 以及更新發布版完全支持 G1 垃圾收集器。

Hotspot 虛擬機包含的垃圾回收器如下:
Hotspot 虛擬機包含的垃圾回收器

垃圾回收在 JVM 中何時觸發

因爲垃圾回收做了分代處理,所以垃圾回收區域和時間也不一樣,GC 有兩種類型:Scavenge GC 和 Full GC。

Scavenge GC

在心對象生成的時候,會在 Eden 區申請空間,若空間申請失敗便觸發 Scavenge GC,對 Eden 區域進行 Scavenge GC,清除非存活的對象,並將存活的對象轉移到 Survivor 區,然後整理兩個 Survivor 區。因爲 Scavenge GC 是在 Eden 區進行的,不會影響到年老代,大部分對象都是從 Eden 區開始的,而 Eden 區空間一般不會分配很大,所以 Eden 區的 GC 很頻繁,一般在該區域需要使用高效快速的回收算法,讓 Eden 區域快速空閒出來。

Full GC

Full GC 是對整個堆區進行 GC ,包括年輕代、年老代和永久代,因此要比 Scavenge GC 要慢,因此儘可能的減少 Full GC 的次數,在 JVM 調優過程中,基本上都是在對 Full GC 進行調節,一般觸發 Full GC 的情況如下:

  • 年老代區域被寫滿
  • 永久代區域被寫滿
  • System.gc() 被調用
  • 上一次 GC 之後 Heap 的各域分配策略動態變化

需要雲服務器的不要錯過優惠

阿里雲低價購買雲服務,值得一看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章