JVM垃圾回收(對象死亡判斷,回收算法,垃圾收集器選擇)

一、 概述

 Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的‘”高牆”,牆外面的人想進去,牆裏面的人卻想出來。說起垃圾回收(GC),大部分人都把這項技術當做Java語言的伴生產物。事實上,GC的歷史比Java久遠,早在1960年Lisp這門語言中就使用了內存動態分配和垃圾回收技術,當Lisp還在胚胎時期時,人們就在思考GC需要完成的3件事情:

  1. 哪些內存需要回收?

  2. 什麼時候回收?

  3. 如何回收?

二、哪些內存需要回收

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

2.1 判斷對象的死亡

2.1.1 引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1(a=b,則b引用的對象實例的計數器+1);當引用失效時,計數器值就減1(a=b,a=c);任何時刻計數器值爲0的對象就是不可能再被使用的,對象死亡。

優點:實現簡單,判定效率高;

缺點無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0。

舉例:

這段代碼是用來驗證引用計數算法不能檢測出循環引用。最後面兩句將object1object2賦值爲null,也就是說object1object2指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不爲0,那麼垃圾收集器就永遠不會回收它們。

2.1.2 可達性分析算法

可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關係看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點,無用的節點將會被判定爲是可回收的對象。

在Java語言中,可作爲GC Roots的對象包括下面幾種:

  1. 虛擬機棧中引用的對象(棧幀中的本地變量表);

  2. 方法區中類靜態屬性引用的對象;

  3. 方法區中常量引用的對象;

  4. 本地方法棧中JNI(Native方法)引用的對象。

2.2 再談引用

在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phanton Refenence)4種,這4種引用強度依次遞減。

 

2.3 生存還是死亡

即使在可達性分析算法中不可達的對象,也並非是“非死不可”,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程。

  第一次標記:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記;

  第二次標記:第一次標記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。在finalize()方法中沒有重新與引用鏈建立關聯關係的,將被進行第二次標記。

  第二次標記成功的對象將真的會被回收,如果對象在finalize()方法中重新與引用鏈建立了關聯關係,那麼將會逃離本次回收,繼續存活。

2.4 回收方法區

很多人認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾回收的,Java虛擬機規範中確實可以不要求虛擬機在方法區實現垃圾收集,而且在方法區中進行垃圾收集的“性價比”一般比較低:在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收70%~80%的空間,而永久代的垃圾收集效率遠低於此。

永久代主要回收兩部分:廢棄常量和無用類。判斷一個常量是否廢棄比較簡單,但是判斷一個類是否是無用類條件相對苛刻:

  1. 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;

  2. 加載該類的ClassLoader已經被回收;

  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

三、常用的垃圾收集算法

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

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

3.2 標記-整理算法(Mark-Compact)

標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

3.3 複製(Copying)算法

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

不過這種算法有個缺點,內存縮小爲了原來的一半,這樣代價太高了。現在的商用虛擬機都採用這種算法來回收新生代,不過研究表明1:1的比例非常不科學,因此新生代的內存被劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活着的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例爲8:1,意思是每次新生代中可用內存空間爲整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion),如下圖

 

3.3 分代收集算法

現代商用虛擬機基本都採用分代收集算法來進行垃圾回收。這種算法沒什麼特別的,無非是上面內容的結合罷了,根據對象的生命週期的不同將內存劃分爲幾塊,然後根據各塊的特點採用最適當的收集算法。大批對象死去、少量對象存活的(新生代),使用複製算法,複製成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),採用標記-清理算法或者標記-整理算法。

四、常用的垃圾收集器及選擇

垃圾收集器就是上面講的理論知識的具體實現了。不同虛擬機所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot,HotSpot這個虛擬機所包含的所有收集器如圖:

上圖展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,那說明它們可以搭配使用。虛擬機所處的區域說明它是屬於新生代收集器還是老年代收集器。多說一句,我們必須明確一個觀點:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot爲什麼要實現這麼多收集器的原因。OK,下面一個一個看一下收集器。

收集器分串行(STW+單線程),並行(STW+多線程)和併發(多線程不需要STW)

 

4.1 Serial收集器(串行)

最基本、發展歷史最久的收集器,這個收集器是一個採用複製算法的單線程的收集器,單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工作,另一方面也意味着它進行垃圾收集時必須暫停其他線程的所有工作,直到它收集結束爲止。後者意味着,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這對很多應用是難以接受的。不過實際上到目前爲止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器,因爲它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。Serial收集器運行過程如下圖所示:

說明:1. 需要STW(Stop The World),停頓時間長。2. 簡單高效,對於單個CPU環境而言,Serial收集器由於沒有線程交互開銷,可以獲取最高的單線程收集效率。

4.2 ParNew收集器(並行)

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其餘行爲和Serial收集器完全一樣,包括使用的也是複製算法。ParNew收集器除了多線程以外和Serial收集器並沒有太多創新的地方,但是它卻是Server模式下的虛擬機首選的新生代收集器,其中有一個很重要的和性能無關的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。CMS收集器是一款幾乎可以認爲有劃時代意義的垃圾收集器,因爲它第一次實現了讓垃圾收集線程與用戶線程基本上同時工作。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。當然,隨着可用CPU數量的增加,它對於GC時系統資源的有效利用還是很有好處的。它默認開啓的收集線程數與CPU數量相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。ParNew收集器運行過程如下圖所示:

4.3 Parallel Scavenge收集器(並行)

 Parallel Scavenge收集器也是一個新生代收集器,也是用複製算法的收集器,也是並行的多線程收集器,但是它的特點是它的關注點和其他收集器不同。介紹這個收集器主要還是介紹吞吐量的概念。CMS等收集器的關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是打到一個可控制的吞吐量。所謂吞吐量的意思就是CPU用於運行用戶代碼時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總運行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器。

停頓時間短適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率利用CPU時間,儘快完成運算任務,主要適合在後臺運算而不需要太多交互的任務。

虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要以爲前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。由於與吞吐量關係密切,Parallel Scavenge收集器也被稱爲“吞吐量優先收集器”。Parallel Scavenge收集器有一個-XX:+UseAdaptiveSizePolicy參數,這是一個開關參數,這個參數打開之後,就不需要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當前系統的運行情況手機性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。如果對於垃圾收集器運作原理不太瞭解,以至於在優化比較困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇。

4.4 Serial Old收集器(串行)

Serial收集器的老年代版本,同樣是一個單線程收集器,使用“標記-整理算法”,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

4.5 Parallel Old收集器(並行)

Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器在JDK 1.6之後的出現,“吞吐量優先收集器”終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合。運行過程如下圖所示:

4.6 CMS收集器(併發)

CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時間爲目標的收集器。使用標記 - 清除算法,收集過程分爲如下四步:

(1). 初始標記,標記GCRoots能直接關聯到的對象,時間很短。

(2). 併發標記,進行GCRoots Tracing(可達性分析)過程,時間很長。

(3). 重新標記,修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,時間較長。

(4). 併發清除,回收內存空間,時間很長。

其中,併發標記與併發清除兩個階段耗時最長,但是可以與用戶線程併發執行。運行過程如下圖所示:

說明:1. 對CPU資源非常敏感,可能會導致應用程序變慢,吞吐率下降。2. 無法處理浮動垃圾,因爲在併發清理階段用戶線程還在運行,自然就會產生新的垃圾,而在此次收集中無法收集他們,只能留到下次收集,這部分垃圾爲浮動垃圾,同時,由於用戶線程併發執行,所以需要預留一部分老年代空間提供併發收集時程序運行使用。3. 由於採用的標記 - 清除算法,會產生大量的內存碎片,不利於大對象的分配,可能會提前觸發一次Full GC。虛擬機提供了-XX:+UseCMSCompactAtFullCollection參數來進行碎片的合併整理過程,這樣會使得停頓時間變長,虛擬機還提供了一個參數配置,-XX:+CMSFullGCsBeforeCompaction,用於設置執行多少次不壓縮的Full GC後,接着來一次帶壓縮的GC。

4.7 G1收集器(併發)

G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中發佈的CMS收集器。與其他GC收集器相比,G1收集器有以下特點:

(1). 並行和併發。使用多個CPU來縮短Stop The World停頓時間,與用戶線程併發執行。

(2). 分代收集。獨立管理整個堆,但是能夠採用不同的方式去處理新創建對象和已經存活了一段時間、熬過多次GC的舊對象,以獲取更好的收集效果。

(3). 空間整合。基於標記 - 整理算法,無內存碎片產生。

(4). 可預測的停頓。能簡歷可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

 在G1之前的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存佈局與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分(可以不連續)Region的集合。

五、是麼時候觸發GC(面試非常容易問!!)

由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有三種類型:Minor GC,Major GC和Full GC。

  1. Minor GC指新生代GC,即發生在新生代(包括Eden區和Survivor區)的垃圾回收操作,當新生代無法爲新生對象分配內存空間的時候,會觸發Minor GC。因爲新生代中大多數對象的生命週期都很短,所以發生Minor GC的頻率很高,雖然它會觸發stop-the-world,但是它的回收速度很快(新生代滿)。

  2. Major GC清理Tenured區,用於回收老年代,出現Major GC通常會出現至少一次Minor GC。

  3. Full GC是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全局範圍的GC。Full GC不等於Major GC,也不等於Minor GC+Major GC,發生Full GC需要看使用了什麼垃圾收集器組合,才能解釋是什麼樣的垃圾回收。

(1). 年老代(Tenured)被寫滿

(2).持久代(Perm)被寫滿;

(3). System.gc()被顯示調用;

(4). 上一次GC之後Heap的各域分配策略動態變化;

您的喜歡是小碼農的動力!加我微信號拉你進java菜雞交流羣,java大廠內推羣,java進階高手羣!

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