一、概述
說起垃圾收集(Garbage Collection),大多數人都會想起Java,這項技術從始至終伴隨着Java的成長,但事實上GC的出現要早於Java,它誕生於1960年MIT的使用動態分配和垃圾回收技術的語言Lisp。經過近60年的發展,目前內存的動態分配和內存回收技術已經非常成熟了,所有的垃圾回收已經自動化,經過迭代更新,自動回收也經過反覆優化,效率和性能都非常可觀。
爲什麼要了解GC?
在你排查內存溢出、內存泄漏等問題時,以及程序性能調優、解決併發場景下垃圾回收造成的性能瓶頸時,就需要對GC機制進行必要的監控和調節。
二、怎樣標識哪些對象“已死”?
既然名叫垃圾回收,那麼哪些對象成爲“垃圾”呢?已經不再被使用的對象便視爲“已死”,就應該被回收。在Java中,GC只針對於堆內存,Java語言中不存在指針說法,而是叫引用,在堆內存中沒有被任何棧內存引用的對象應該被回收。
1.引用計數算法
引用計數算法是判斷對象是否存活的算法之一:它給每一個對象加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能被使用的,即將被垃圾回收器回收。
缺點:無法解決對象減互相循環引用的問題。即當兩個對象循環引用時,引用計數器都爲1,當對象週期結束後應該被回收卻無法回收,造成內存泄漏。
public class GcTest {
public static void main(String[] args) {
MyObject myObject_1 = new MyObject();
MyObject myObject_2 = new MyObject();
myObject_1.instance = myObject_2;
myObject_2.instance = myObject_1;
myObject_1 = null;
myObject_2 = null;
// 對象循環引用,當時用引用計數算法時,無法回收這兩個對象
System.gc();
}
static class MyObject{
Object instance;
}
}
2.可達性分析算法
目前主流使用的都是可達性分析算法來判斷對象是否存活。算法基本思路:以“GC Roots”作爲對象的起點,從此節點開始向下搜索,搜索所走過的路徑成爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
哪些對象可作爲GC Roots?
-
虛擬機棧(棧幀中的本地變量表)中引用的對象;
-
方法區中類靜態屬性引用的對象;
-
方法區中常量引用的對象;
-
本地方法棧中JNI(Native方法)引用的對象;
-
活躍線程的引用對象。
三、Java中四種引用
在JDK1.2之前,Java中的引用定義很單一:如果reference類型的數據中儲存的數值代表的是另一塊內存的起始地址,就稱這塊內存代表着一個引用。但是這種定義太過狹隘,如果某個對象介於被引用和未被引用兩種狀態之間,那麼這種定義就顯得無能爲力。在JDK1.2後Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference),這四種引用強度依次逐漸減弱。
-
強引用(Strong Reference)
強引用就是值在程序代碼中普遍存在的,用new關鍵字創建的對象都是強引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
-
軟引用(Soft Reference)
軟引用是用來描述一些還有用但並非必需的對象,在系統將要發生內存溢出之前,將會吧這些對象列入回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。可用來實現高速緩存。軟引用對象在回收時會被放入引用隊列(ReferenceQueue)。
// 軟引用
SoftReference<String> softReference = new SoftReference<>("北風IT之路");
-
弱引用(Weak Reference)
弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,**被弱引用關聯的對象只能生存到下一次GC發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉該類對象。**弱引用對象在回收時會被放入引用隊列(ReferenceQueue)。
// 弱引用
WeakReference<String> weakReference = new WeakReference<>("北風IT之路");
-
虛引用(Phantom Reference)
虛引用被稱爲幽靈引用或幻象引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得對象實例。任何時候都可能被回收,一般用來跟蹤對象被垃圾收集器回收的活動,起哨兵作用。必須和引用隊列(ReferenceQueue)聯合使用。
// 虛引用,必須配合引用隊列使用
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>("北風IT之路",referenceQueue);
四、finalize()賦予對象重生
在可達性分析算法中被標記爲不可達的對象,也不一定是一定會被回收,它還有第二次重生的機會。每一個對象在被回收之前要進行兩次標記,一次是沒有關聯引用鏈會被標記一次,第二次是判斷該對象是否覆蓋finalize()方法,如果沒有覆蓋則真正的被定了“死刑”。
如果這個對象被jvm判定爲有必要執行finalize()方法,那麼這個對象會被放入F-Queue隊列中,並在燒燬由一個由虛擬機自動創建的、低優先級的finalizer線程去執行它。但是這裏的“執行”是指虛擬機會觸發這個方法,但是**並不代表會等它運行結束。**虛擬機在此處是做了優化的,因爲如果某個對象在finalize方法中長時間運行或者發送死循環,將可能導致F-Queue隊列中其他對象永遠處於等待,甚至可能會導致整個內存回收系統崩潰。如果要在finalize方法中重生這個對象你可以按照下面代碼做:
public class GcTest {
public static GcTest instance = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("收集器檢測到finalize方法,對象即將獲得一次重生的機會");
instance = this;
}
public static void main(String[] args) throws InterruptedException{
instance = new GcTest();
// 引用置爲空,堆內對象將視爲垃圾
instance = null;
// 執行gc
System.gc();
Thread.sleep(500);
// 雖然執行了gc,但是可能在finalize方法中獲得重生,
// 因此可能會打印出myObject的地址
System.out.println(instance);
// 最後打印出jvm.GcTest@7cc355be
}
}
注意!finalize()方法只會被系統調用一次,多次被gc只有第一次會被調用,因此只有一次的重生機會。
五、回收方法區
假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是“abc”,那麼這個對象就應該回收。方法去(HotSpot虛擬機中的永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。比如上述的“abc”就是屬於廢棄常量,那麼哪些類是無用的類呢?
-
該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
-
加載該類的ClassLoader已經被回收;
-
該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
六、垃圾收集算法
1.標記-清理算法(Mark-Sweep)
算法思路:算法分爲“標記”和“清理”兩個步驟,首先標記處所有需要回收的對象,在標記完成後再統一回收所有被標記的對象。
缺陷:
-
標記和清理的兩個過程效率都不高;
-
容易產生內存碎片,碎片空間太多可能導致無法存放大對象。
適用於存活對象佔多數的情況。
圖片來源:https://cloud.tencent.com/developer/article/1336613
2.複製算法(Copy)
算法思路:將可用內存劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完後,就將還存活的對象複製到另一塊去,然後再把已使用過的內存空間一次清理掉。
缺陷:
-
可用內存縮小爲了原來的一半
算法執行效率高,適用於存活對象佔少數的情況。
圖片來源:https://cloud.tencent.com/developer/article/1336613
3.標記-整理算法(Mark-compact)
算法思路:標記過程和標記-清理算法一樣,而後面的不一樣,它是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
有效地避免了內存碎片的產生。
4.分代收集算法(Generational Collection)
當前大多數垃圾收集都採用的分代收集算法,這種算法並沒有什麼新的思路,只是根據對象存活週期的不同將內存劃分爲幾塊,每一塊使用不同的上述算法去收集。**在jdk8以前分爲三代:年輕代、老年代、永久代。在jdk8以後取消了永久代的說法,而是元空間取而代之。**一般年輕代使用複製算法(對象存活率低),老年代使用標記整理算法(對象存活率高)。
4.1 年輕代(複製算法爲主)
儘可能快的收集掉聲明週期短的對象。整個年輕代佔1/3的堆空間,年輕代分爲三個區,Eden、Survivor-from、Survivor-to,其內存大小默認比例爲8:1:1(可調整),大部分新創建的對象都是在Eden區創建。當回收時,先將Eden區存活對象複製到一個Survivor-from區,然後清空Eden區,存活的對象年齡+1;當這個Survivor-from區也存放滿了時,則將Eden區和Survivor-from區存活對象複製到另一個Survivor-to區,然後清空Eden和這個Survivor-from區,存活的對象年齡+1;此時Survivor-from區是空的,然後將Survivor-from區和Survivor-to區交換,即保持Survivor-from區爲空(此時的Survivor-from是原來的Survivor-to區), 如此往復。年輕代執行的GC是Minor GC。
年輕代的迭代更新很快,大多數對象的存活時間都比較短,所以對GC的效率和性能要求較高,因此使用複製算法,同時這樣劃分爲三個區域,保證了每次GC僅浪費10%的內存,內存利用率也有所提高。
4.2 老年代(標記-整理算法爲主)
在年輕代經過很多次垃圾回收之後仍然存活的對象(默認15歲),就會被放入老年代中,因爲老年代中的對象大多數是存活的,所以使用算法是標記-整理算法。老年代執行的GC是Full GC。
4.3 永久代/元空間
jdk8以前:
永久代用於存放靜態文件,如Java類、方法等。該區域回收與上述“方法區內存回收”一致。但是永久代是使用的堆內存,如果創建對象太多容易造成內存溢出OOM(OutOfMemory)。
jdk8以後:
jdk8以後便取消了永久代的說法,而是用元空間代替,所存內容沒有變化,只是存儲的地址有所改變,元空間使用的是主機內存,而不是堆內存,元空間的大小限制受主機內存限制,這樣有效的避免了創建大量對象時發生內存溢出的情況。
七、Minor GC和Full GC
之前多次提到Minor GC和Full GC,那麼它們有什麼區別呢?
-
Minor GC即新生代GC:發生在新生代的垃圾收集動作,因爲Java有朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
-
Major GC / Full GC:發生在老年代,經常會伴隨至少一次Minor GC。Major GC的速度一般會比Minor GC慢倍以上。
Minor GC發生條件:
-
當新對象生成,並且在Eden申請空間失敗時;
Full GC發生條件:
-
老年代空間不足
-
永久帶空間不足(jdk8以前)
-
System.gc()被顯示調用
-
Minor GC晉升到老年代的平均大小大於老年代的剩餘空間
-
使用RMI來進行RPC或管理的JDK應用,每小時執行1次Full GC
八、常見的垃圾收集器(jdk8及以前)
一張圖即可清除看到不同垃圾收集器之間的關係,連線表示可以配合使用。
-
Serial收集器(複製算法)
新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定。
-
Serial Old收集器(標記-整理算法)
老年代單線程收集器,Serial收集器的老年代版本。
-
ParNew收集器(複製算法)
新生代收集器,Serial收集器的多線程版本,在多核CPU情況時表現更好。
-
Parallel Scavenge收集器(複製算法)
並行收集器,追求高吞吐量,高效利用CPU。適合後臺應用等對交互相應要求不高的場景。是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=2來指定線程數。
-
Parallel Old收集器(複製算法) Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先。
-
CMS(Concurrent Mark Sweep)收集器(標記-清理算法) 高併發、低停頓,追求最短GC回收停頓時間(Stop The World),cpu佔用比較高,響應時間快,停頓時間短,多核cpu追求高響應時間的選擇,但是因爲使用標記清理算法,容易產生內存碎片。
-
G1收集器
G1是一款面向服務端應用的垃圾收集器,支持並行與併發、分代收集、空間整合和可預測停頓的能力,即可適用於年輕代又可適用於老年代。
圖片來源:https://cloud.tencent.com/developer/article/1336613
九、垃圾收集器參數總結
-
UseSerialGC:虛擬機運行在Client模式下的默認值,打開此開關後,使用Serial+Serial Old的收集器組合進行內存回收
-
UseParNewGC:打開此開關後,使用ParNew+Serial Old的收集器組合進行內存回收
-
UseConcMarkSweepGC:打開此開關後,使用ParNew+CMS+Serial Old的收集器組合進行內存回收。Serial Old收集器將作爲CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用
-
UseParallelGC:虛擬機運行在Server模式下的默認值,打開此開關後,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行內存回收
-
UseParallelOldGC:打開此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行內存回收
-
SurvivorRatio:新生代中Eden區域與Survivor區域的容量比值,默認值爲8,代表Eden:Survivor=8:1
-
PretenureSizeThreshold:直接晉升到老年代的對象大小,設置這個參數後,大於這個參數的對象將直接在老年代分配
-
MaxTenuringThreshold:晉升到老年代的對象年齡,每個對象在堅持過一次Minor GC之後,年齡就增加1,當超過這個參數時就進入老年代
-
UseAdaptiveSizePolicy:動態調整Java堆中各個區域的大小以及進入老年代的年齡
-
HandlePromotionFailure:是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有對象都存活的極端情況
-
ParallelGCThreads:設置並行GC時進行內存回收的線程數
-
GCTimeRatio:GC時間佔總時間的比率,默認值爲99,即允許1%的GC時間。僅在使用Parallel Scavenge收集器時生效
-
MaxGCPauseMillis:設置GC的最大停頓時間,僅在使用Parallel Scavenge收集器時生效
-
CMSInitingOccupancyFraction:設置CMS收集器在老年代空間被使用多少後觸發垃圾收集。默認值爲68%,僅在使用CMS收集器時生效
-
UseCMSCompactAtFullCollection:設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片整理,僅在使用CMS收集器時生效
-
CMSFullGCsBeforeCompaction:設置CMS收集器在進行若干次垃圾收集後再啓動一次內存碎片整理。僅在使用CMS收集器時生效
參考文獻:《深入理解Java虛擬機》