java虛擬機-垃圾回收機制

如何確定對象已死

“死”說的就是沒用的對象,那麼如何確定已經死亡的對象呢!有下面兩個方法

引用計數法

實現原理就是在對象中增加一個引用計數器,當計數器爲零時,說明對象已死可以被回收了。

但是由於解決不了循環引用的問題,就是A引用B、B引用C、C在引用A
這樣他們永遠不會被回收,所以java並沒有使用此方法,java中用到的方法是下面的

根搜索法

根搜索算法的基本思路是通過一系列的“GC Roots”的對象作爲起始點,從這些節點開始往下搜索,搜索的走過的路徑稱爲引用鏈。

當一個對象到“GC Roots”沒有引用鏈可達時(也就是用圖論的話說就是從GC Roots到這個對象不可達),則證明此對象是不可用的,這樣的對象被判定爲是可回收的。

java中可以作爲GC Roots對象包括以下幾種:

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

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

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

4.本地方法棧中JNI(也即一般說的Native方法)的引用的對象。

根搜索算法判斷對象是否存活與引用有關。java將引用分爲四類:強引用、軟引用、弱引用、虛引用,這四種引用強度依次逐步減弱。

強引用: 類似new出來的對象,只要引用還在永遠不會被回收
軟引用: 用來描述還有點用但非必須的對象,在內存溢出時將會把這些對象進行回收。
弱引用: 被弱引用的對象只能活到下一次GC發生之前,典型的例子就是觸發finalize()過後的對象,如有需要對象都可以重寫finalize()方法躲過一次GC
虛引用: 被虛引用肯定會被回收的。

垃圾回收算法

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

在這裏插入圖片描述
標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。

但是這種算法很容易產生碎片,碎片太多可能會導致後續過程中需要爲大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。而且效率也不高

Copying(複製)算法

在這裏插入圖片描述
它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因爲能夠使用的內存縮減到原來的一半。

很顯然,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。是新生代常用的算法

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

在這裏插入圖片描述
算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。是老年代常用的算法

Generational Collection(分代收集)算法

這是現在java採用的算法,將堆分爲新生代和老年代,根據各個年代的特點分別採用不同的算法。

老年代的特點是每次垃圾收集時只有少量對象需要被回收,所以使用標記-整理算法,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收所以使用複製算法,前面的博文也講過新生代是分爲三個區的。

垃圾收集器

Serial

在jdk1.3.1之前是唯一的選擇,是一個單線程的收集器。

Serial收集器是針對新生代的收集器,採用的是Copying算法
Serial Old收集器是針對老年代的收集器,採用的是Mark-Compact算法。

它的優點是實現簡單高效,但是缺點是會給用戶帶來停頓,相當於看電視高潮的時候卡屏了,想想那種感覺把!

使用方式:-XX:+UseSerialGC,打開該開關後,使用Serial(年輕代)+Serial Old(老年代) 組合進行GC。

ParNew

並行收集器,Serial的多線程版本,使用多條線程進行垃圾回收,其他特性與Serial一致。需要注意的是,ParNew在單核甚至雙核環境下絕對不會有比Serial收集器更好的效果,但是隨着CPU數量的增加ParNew相較於Serial的優勢會越來越明顯,但並不是成倍增長的,原因還是那個,多線程切換開銷。

另外ParNew用於垃圾回收的線程可用參數-XX:ParallelGCThreads=n進行配置。建議n與主機邏輯cpu數一致。

使用方式:-XX:+UseParNewGC,打開該開關後,使用ParNew(年輕代)+Serial Old(老年代)組合進行GC。另外,ParNew是CMS收集器的默認年輕代收集器。

Parallel Scavenge

Parallel Scavenge新生代多線程收集器,使用Copying算法
Parallel Old 老年代多線程收集器,使用Mark-Compact算法

Parallel Scavenge收集器的關注點與其他收集器不同, ParallelScavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

由於與吞吐量關係密切,Parallel Scavenge收集器也經常被稱爲“吞吐量優先”收集器。

該垃圾收集器,是JAVA虛擬機在Server模式下的默認值,使用Server模式後,java虛擬機使用Parallel Scavenge收集器(新生代)+ Serial Old收集器(老年代)的收集器組合進行內存回收。

重要的參數有三個,其中兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數及直接設置吞吐量大小的 -XX:GCTimeRatio參數。另外一個是UseAdaptiveSizePolicy開關參數。

當這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)。

CMS

Concurrent Mark-Sweep 很明顯是採用標記-清除算法的併發收集器,用來收集老年代的垃圾減少full gc發生。

在啓動JVM參數加上-XX:+UseConcMarkSweepGC表示使用CMS

CMS運行過程:

  • 初始標記(STW initial mark) ***暫停應用
  • 併發標記(Concurrent marking)
  • 併發預清理(Concurrent precleaning)
  • 重新標記(STW remark) *** 暫停 應用
  • 併發清理(Concurrent sweeping)
  • 併發重置(Concurrent reset)

初始標記 : 在這個階段,需要虛擬機停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的"根對象"開始,只掃描到能夠和"根對象"直接關聯的對象,並作標記。所以這個過程雖然暫停了整個JVM,但是很快就完成了。

併發標記 : 這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程序的線程和併發標記的線程併發執行,所以用戶不會感受到停頓。

併發預清理 : 併發預清理階段仍然是併發的。在這個階段,虛擬機查找在執行併發標記階段新進入老年代的對象(可能會有一些對象從新生代晉升到老年代, 或者有一些對象被分配到老年代)。通過重新掃描,減少下一個階段"重新標記"的工作,因爲下一個階段會Stop The World。

重新標記 : 這個階段會暫停虛擬機,收集器線程掃描在CMS堆中剩餘的對象。掃描從"跟對象"開始向下追溯,並處理對象關聯。

併發清理 : 清理垃圾對象,這個階段收集器線程和應用程序線程併發執行。

併發重置 : 這個階段,重置CMS收集器的數據結構,等待下一次垃圾回收。

在這裏插入圖片描述

缺點:
1.因爲CMS標記階段應用程序的線程還是在執行的,那麼就會有堆空間繼續分配的情況。爲了保證在CMS回 收完堆之前還有空間分配給正在運行的應用程序,必須預留一部分空間。
而採用標記清除算法會產生碎片,所以老年代明明還有很多空間卻因爲碎片無法進行進行分配,不得不提前觸發full gc,所以不能把– XX:CMSInitiatingOccupancyFraction設置的太高

2.很佔CPU資源。爲了讓應用程序不停頓,CMS線程和應用程序線程併發執行,這樣就需要有更多的CPU,單純靠線程切 換是不靠譜的。並且,重新標記階段,爲空保證STW快速完成,也要用到更多的甚至所有的CPU資源。

G1

G1把堆重新劃分沒有整塊的新生代和老年代了,而且多了一個Humongous區域,屬於一個全新的垃圾收集器,簡單看下劃分
在這裏插入圖片描述
從整體來看算是標記整理的算法,從局部來看算是複製算法,所以它不會產生碎片的缺點。

而且G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行。
G1運行過程:

1、初始標記;
2、併發標記;
3、最終標記;
4、篩選回收;

上面幾個步驟的運作過程和CMS有很多相似之處。

初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS的值,讓下一個階段用戶程序併發運行時,能在正確可用的Region中創建新對象,這一階段需要停頓線程,但是耗時很短。

併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段時耗時較長,但可與用戶程序併發執行。

而最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remenbered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set中,這一階段需要停頓線程,但是可並行執行。

最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

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