垃圾回收機制 —— 整理介紹

垃圾回收機制的意義

在 C++ 開發中管理內存是一個很麻煩的問題,而 Java 引入了垃圾回收機制,開發者不須要手動去管理內存的分配和回收問題,一切都交給 JVM 經過垃圾回收機制處理,同時有效的防止了內存泄漏的問題。java

Java 語言規範中並無明確的指定 JVM 使用哪一種回收算法,但一般回收算法主要作 2 件事情:算法

  • 發現無用的對象
  • 回收被無用對象佔用的內存空間

如何發現無用的對象

Reference Counting(引用計數)

早期的 JVM 利用的策略是引用計數。通常來講,堆中的每個對象對應一個引用計數器。數組

  • 當建立一個對象並分配給一個引用變量時,對象的引用計數器置爲 1。
  • 當任何其餘引用變量被賦值爲這個對象的引用時,引用計數器加 1。
  • 但當一個對象的某個引用變量超過了生命週期或者被設置爲一個新值時,該對象的引用計數器減 1。
  • 當一個對象被回收時,它引用的任何對象的引用計數器都減 1。
  • 任何引用計數器爲 0 的對象能夠被看成無用的對象。

利用這種方法判斷無用的對象,實現簡單高效,對程序須要不被長時間打斷的環境比較有利。但這種方法沒法解決循環引用的問題:多線程

Object o1 = new Object();
Object o2 = new Object();

o1.object = o2;
o2.object = o1;

o1 = null;
o2 = null;

o1,o2 最後都被賦值爲 null,也就是說以前 o1,o2 所引用的對象都沒法被訪問。可是因爲兩個對象互相引用對方,因此它們的引用計數器都不爲 0,因此垃圾收集器沒法回收它們。併發

Tracing(追蹤)算法

如今垃圾回收機制都使用根搜索算法,把全部的引用關係看做一張圖,根集(root set)做爲圖的起點,所謂根集就是正在執行的 Java 程序能夠訪問的引用變量的集合(包括局部變量、參數、類變量)。從根集 開始,尋找可達的對象,找到可達的對象後繼續尋找這個對象的引用對象,當全部的可達的或間接可達的對象尋找完畢,剩餘的則被認爲是不可達的遊離對象,即無用的對象。線程

image

典型的垃圾收集算法

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

這是最基礎的垃圾收集算法,標記 - 清除算法是從根集進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收。該方法不移動對象,僅僅回收未標記的對象,在存活對象較多的狀況下效率極高。可是這個算法也容易產生內存碎片,過多的內存碎片會致使爲大對象分配空間時沒法找到足夠的空間,而觸發新一次的垃圾收集動做。code

image

2. Mark - Compact(標記 - 整理)算法

標記 - 整理算法在標記階段與標記 - 清除算法一致,可是爲了解決內存碎片的問題,在完成標記後,並不直接清理未標記的對象,而是將存活的對象都向一端移動,而後清理掉存活對象端邊界之外的內存。通常在這種算法的實現中,都增長了句柄和句柄表,也形成了必定的開銷。對象

image

3. Copying(複製)算法

複製算法會將堆內存分爲使用區和空閒區兩部分。每次只使用其中的使用區,當使用區用完,就進行一次掃描標記,將還存活的對象複製到空閒區上,而後再將使存區進行一次清理。這樣,空閒區成爲了使用區,原來的使用區變成了空閒區。這鐘也解決了內存碎片的問題。一種典型的基於 Copying 算法的垃圾回收是 Stop - Copy 算法,它在使用區和空閒區的切換過程當中,程序暫停執行。blog

這種算法雖然簡單高效,且不易產生內存碎片,卻對內存空間的利用付出了高昂的代價,內存使用率只有一半。並且,存活的對象若是數量居多,那麼算法效率將大大下降。生命週期

image

4. Generational(分代)算法

分代收集算法是目前大部分 JVM 的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期不一樣將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲新生代(Young Generation)和老年代(Tenured Generation)。不一樣生命週期的對象能夠採起不一樣的回收算法,以提升回收效率。

image

新生代(Young Generation)

全部新生成的對象首先都是放在新生代中,在新生代的目標是儘量快的收集生命週期短暫的對象。

目前大部分垃圾收集器對於新生代都採用 Copying 算法,由於新生代中每次都要回收大部分對象,存活的對象較少,因此複製操做較少。通常來講,新生代的內存按照 8:1:1 的比例劃分爲一個 Eden 區和兩個較小的 Survivor0,Survivor1 區。大部分對象在 Eden 區生成,回收時先將 Eden 區中存活的對象複製到一個 Survivor0 區中,而後清空 Eden 區。當這個 Survivor0 區也存放滿時,則將 Eden 區和 Survivor0 區的存活對象複製到另外一個 Survivor1 區,而後清空 Eden 和 Survivor0 區,此時 Survivor0 區是空的,而後將 Survivor0 區和 Survivor1 區交換,即保持 Survivor1 區爲空,如此往復。

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

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

老年代(Tenured Generation)

在年輕代中經歷了屢次垃圾回收後仍然存活的對象,到達必定次數後就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。因此每次回收都只回收少許對象,通常使用的是 Mark - Compact 算法。

通常來講,大對象會被直接分配到老年代,所謂的大對象是指須要大量連續存儲空間的對象,最多見的一種大對象就是大數組。

老年代內存比新生代也大不少,當老年代內存滿時觸發 Major GC 即 Full GC,發生的頻率比較低。

持久代(Permanent Generation)

在堆區以外還有一個就是持久代(Permanent Generation),它用於存放靜態文件,如 class 類、常量、方法描述等。持久代的回收主要回收兩部份內容:廢棄的常量和無用的類。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些 class,例如 Hibernate 等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。

典型的垃圾收集器

垃圾收集算法是內存回收的理論基礎,而垃圾收集器就是內存回收的具體實現。下面 JVM 提供的幾種垃圾收集器,用戶能夠根據本身的需求組合出各個年代使用的收集器。

image

1. Serial/Serial Old

它們都是一個單線程的收集器,在進行垃圾收集時,必須暫停全部的用戶線程。它的優勢是實現簡單高效,可是缺點是會給用戶帶來停頓。Serial 是針對新生代的收集器,採用的是 Copying 算法。Serial Old 是針對老年代的收集器,採用的是 Mark - Compact 算法。

2. ParNew

ParNew 收集器是 Serial 收集器的多線程版本,使用多個線程進行垃圾收集,是針對新生代的收集器,採用的是 Stop - Copy 算法。

3. Parallel Scavenge / Parallel Old

Parallel Scavenge 收集器是一個針對新生代的多線程收集器(並行收集器),它在回收期間不須要暫停其餘用戶線程,其採用的是 Copying 算法,該收集器與前兩個收集器有所不一樣,它主要是爲了達到一個可控的吞吐量。Parallel Old 是 Parallel Scavenge 收集器的老年代版本(並行收集器),使用多線程和 Mark - Compact 算法。

4. CMS(Concurrent Mark Sweep)

它是一種以獲取最短回收停頓時間爲目標的收集器,它是一種針對老年代的併發收集器,採用的是 Mark - Sweep 算法。

5. G1

G1 收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多 CPU、多核環境。所以它是一款並行與併發收集器,而且它能創建可預測的停頓時間模型。

垃圾回收執行機制的分類

因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC 有兩種類型:Scavenge GC 和 Full GC。

Scavenge GC

通常狀況下,當新對象生成,而且在 Eden 申請空間失敗時,就會觸發 Scavenge GC,對 Eden 區域進行 GC,清除非存活對象,而且把尚且存活的對象移動到 Survivor 區,而後整理 Survivor 的兩個區。這種方式的 GC 是對年輕代的 Eden 區進行,不會影響到老年代。由於大部分對象都是從 Eden 區開始的,同時 Eden 區不會分配的很大,因此 Eden 區的 GC 會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使 Eden 去能儘快空閒出來。

Full GC

對全部年代進行整理,包括新生代、老年代和持久代。Full GC 由於須要對整個內存進行回收,因此比 Scavenge GC 要慢,所以應該儘量減小 Full GC 的次數。在對 JVM 調優的過程當中,很大一部分工做就是對於 Full GC 的調節。有以下緣由可能致使 Full GC:

  • 年老代被寫滿
  • 持久代被寫滿
  • System.gc() 被顯示調用
  • 上一次 GC 以後堆中的各域分配策略動態變化
相關文章
相關標籤/搜索