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


垃圾回收機制的意義

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

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(標記 - 清除)算法

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

image

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

標記 - 整理算法在標記階段與標記 - 清除算法一致,但是為了解決內存碎片的問題,在完成標記后,並不直接清理未標記的對象,而是將存活的對象都向一端移動,然后清理掉存活對象端邊界以外的內存。一般在這種算法的實現中,都增加了句柄和句柄表,也造成了一定的開銷。

image

3. Copying(復制)算法

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

這種算法雖然簡單高效,且不易產生內存碎片,卻對內存空間的利用付出了高昂的代價,內存使用率只有一半。而且,存活的對象如果數量居多,那么算法效率將大大降低。

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 之后堆中的各域分配策略動態變化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM