JVM 垃圾收集器(Garbage Collection)
判斷對象是否存活
在堆里邊存放著java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,首先需要確定這些對象之中哪些還“存活”著,哪些已經“死去”(即不可能再被任何途徑使用的對象)。
引用計數算法
給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。該方法簡單,但也有一個缺點是很難解決對象之間相互循環引用的問題。
可達性分析算法
該算法的基本思路就是通過一系列的稱為“GCRoots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。在java語言中,可作為GC Roots的對象包括下面幾種:
再談引用
引用分為強引用、軟引用、弱引用和虛引用。
- 強引用:代碼中明確指明的,如“Object a = new Object()”,只要強引用還在,就不會被GC
- 軟引用:被軟應用關聯對象第一次發生GC時可以躲過,第二次遇到GC時才會被回收
- 弱引用:被弱引用關聯的對象在發生GC時就會被回收
- 虛引用:也稱為幽靈引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象唄垃圾收集器回收時受到一個系統通知。
生存還是死亡
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的。要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。
如果這個對象唄判定為有必要執行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列中,并在稍后由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要再finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那么在第二次標記時它將被移除出“即將回收”的集合。
回收方法區
方法區(或者HotSpot虛擬機中的永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收java堆中的對象非常相似。以常量池中字面量的回收為例,假設一個字符串”abc”已經進入了常量池中,但是當前系統沒有任何一個String對象值為”abc”,換句話說,就是沒有任何String對象引用常量池中的”abc”常量,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話,這個”abc”常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻的多,需要同時滿足一下3個條件:
在大量使用發射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
垃圾收集算法
1. 標記-清除算法(Mark-Sweep)?
首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。這種算法的主要不足有兩個:一個是效率問題,標記和清楚兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前出發另一次垃圾收集動作。
2. 復制算法(Copying)
復制算法適用于新生代,因為在新生代,垃圾對象通常會多余存活對象,復制算法效果會比較好。為了解決效率問題,可以將內存按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活的對象復制到另一塊上面,然后再把已使用過的內存空間一次清理掉。這種方法實現簡單,運行高效,代價是將內存縮小為了原來的一半。
為了提高內存的利用率,可以將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛剛用過的Survivor空間。
為什么需要有兩塊Survivor? 這是因為survivor中的對象在達到“老年”(由指定參數-XX:MaxTenuringThreshold決定)之前肯定有對象已經變成“垃圾”了,這時候必須要對其進行回收,如果只使用一個survivor的話,那么要不容忍survivor存在內存碎片,要么要對其進行內存整理,出于和對Eden區域同樣的考慮,所以實際上對Survivor的GC也是基于復制算法的,不過是從一個Survivor到另外一個Survivor(這也是GC日志中為什么叫from space和to space),所以Survivor的兩個區是對稱的,沒有先后關系,所以Survivor區中可能同時存在從Eden復制過來對象,以及從前一個Survivor復制過來的對象,某一次GC結束時肯定會有一個Survivor是空的。
3. 標記-整理算法(Mark-Compact)
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出“標記-整理”算法,標記過程仍然與“標記-清除”算法中的一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
4. 分代收集算法
一般把JVM的堆分為新生代和老年代,這樣可以根據各個年代的特點采用最適當的收集算法。在新生代中,選用復制算法;而老年代中因為對象存活率高,沒有額外空間對它進行分配擔保,就采用“標記-清理”或者“標記-整理”算法來進行回收。
5. 分區收集算法
分區收集算法是將整個堆空間劃分為連續的不同小區間,每個小區間獨立使用,獨立回收,G1收集器就是使用該算法。
HotSpot中垃圾收集算法的實現
枚舉根節點
在可達性分析中,從GC Roots節點找引用鏈這個操作為例,可作為GC Roots的節點主要在全局性的引用(例如常量或者類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這里面的引用,那么必然會消耗很多時間。
另外,可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這里“一致性”的意思是指在整個分析起見整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用關系還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。
對于主流的Java虛擬機,當執行系統停頓下來后,并不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應該是有辦法直接得知哪些地方存放著對象引用。對于HotSpot,是使用了一組稱為OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什么偏移量上是什么類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。
安全點
在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉,但一個很現實的問題隨之而來:可能導致引用關系變化,或者說OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。
實際上,HotSpot只是在“特定的位置”記錄了這些信息,這些位置成為安全點。即程序執行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。安全點的選定既不能太少以至于讓GC等待時間太長,也不能過于頻繁以至于過分增大運行時的負荷。所以安全點的選擇基本上是以程序“是否具有讓程序長時間執行的特征”為標準。“長時間執行”的最明顯特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等。
對于安全點,另一個需要考慮從的問題是如何在GC發生時讓所有線程都“跑”到最近的安全點上再停頓下來。這里有兩種方案:搶先式中斷和主動式中斷(使用較多)。搶先式中斷是在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上;而主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時,主動去輪詢這個標志,發現中斷標志位真時就自己中斷掛起。
安全區域
安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的安全點。但是,程序“不執行”的時候呢?比如沒有分配CPU時間,典型的例子就是線程處于Sleep或者Blocked狀態,這時候線程無法響應JVM的中斷請求,從而“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間,對于這種情況,就需要安全區域來解決。
安全區域是指一段代碼片段之中,引用關系不會發生變化。在這個區域中的任何地方開始GC都是安全的。在線程執行到安全區域后,首先標識自己已經進入安全區域,當在這段時間里JVM要發起GC時,就不用管標識自己為安全區域狀態的線程了。在線程要離開安全區域時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待知道收到可以安全離開安全區域的信號為止。
垃圾收集器
串行收集器
新生代串行收集器Serial GC是基于復制算法,實現簡單,處理高效。老年代串行收集器Serial Old GC使用的是標記整理算法。Serial收集器是最簡單的一個,是一個單線程的收集器,它在工作的時候會將所有應用線程全部凍結。
如何使用它:你可以打開-XX:+UseSerialGC這個JVM參數來使用它。
?
?
與串行收集器相關的參數:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:設置Eden區大小和Survivor區大小的比例;默認是8,即Eden:Survivor=8:1
-XX:PretenureSizeThreshold:設置大對象直接進入老年代的對象大小閾值。
-XX:MaxTenuringThreshold:設置對象進入老年代的年齡的最大值;默認是15歲,出生就已經是1歲了。
并行收集器
ParNew收集器
新生代ParNew收集器ParNewGC是串行收集器的多線程版本
則老年代默認使用SerialOldGC收集器,老年代也可以修改成CMS
Parallel Scavenge收集器
新生代ParallelGC收集器也是使用復制算法,但它非常關注系統的吞吐量。
老年代ParallelOldGC收集器也是一種關注吞吐量的收集器,使用標記-整理算法。
所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值。吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。
與并行GC相關的參數:
-XX:+UseParNewGC:在新生代使用并行收集器;
-XX:+UseParallelGC:設置并行新生代收集器(吞吐量優先)
-XX:+UseParallelOldGC:老年代使用并行收集器;
-XX:ParallelGCThreads:設置用于垃圾回收的線程數。通常和CPU數量相等;
-XX:MaxGCPauseMillis:設置最大垃圾收集停頓時間;
-XX:GCTimeRatio:設置吞吐量大小,0到100之間;默認是99,如99就代表:運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)=99%
CMS收集器
CMS收集器(concurrent-mark-sweep)是一種以獲取最短回收停頓時間為目標的收集器。其使用了多個線程(concurrent)來掃描堆并標記(mark)那些不再使用的可以回收(sweep)的對象。主要步驟有:初始標記、并發標記、預清理、重新標記、并發清除和并發重置。這個算法在兩種情況下會進入一個”stop the world”的模式:當進行根對象的初始標記的時候 (老生代中線程入口點或靜態變量可達的那些對象)以及當這個算法在并發運行的時候應用程序改變了堆的狀態使得它不得不回去再次確認自己標記的對象都是正確的。
雖然稱之為并發低停頓收集器,但是它有以下3個缺點:
G1回收器
G1( Garbage first)回收器在JDK 7update 4中首次引入,與其他收集器相比,G1具有以下特點:
并行與并發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU或者CPU核心來縮短Stop-The-World停頓的時間;
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留;
空間整合:與CMS的“標記-清理”算法不同,G1從整體來看是基于“標記-整理”算法實現的收集器,從局部來看是基于“復制”算法實現的,兩種算法都不會產生內存空間碎片;
可預測的停頓:降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集的時間不得超過N毫秒。
G1算法將堆劃分為若干個區域(Region),它仍然屬于分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。
在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
詳細請看:https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://blog.csdn.net/j3T9Z7H/article/details/80074460
相關參數:
-XX:+UseG1GC:使用G1收集器
-XX:MaxGCPauseMillis:設置最大垃圾收集停頓時間;
-XX:G1HeapRegionSize:使用G1時Java堆會被分為大小統一的的區(region)。此參數可以指定每個heap區的大小. 默認值將根據 heap size 算出最優解. 最小值為 1Mb, 最大值為 32Mb;
?
如果你追求低停頓,那G1是個不錯的選擇。雖然G1沒有太犧牲吞吐量,但如果你追求吞吐量,那么G1并不會為你帶來什么特別的好處。
?
擴展閱讀:HotSpot JVM默認垃圾收集器
總結
以上是生活随笔為你收集整理的JVM 垃圾收集器(Garbage Collection)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 8086指令系统中的寻址方式
- 下一篇: matlab自动变量名,matlab中如