3.垃圾回收器

3.1.引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的。

但是,至少主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。

3.2.可達性分析算法

這個算法的基本思路就是通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的

 

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

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

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

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

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

 

對象死亡過程:

對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queue的隊列之中,執行finalize()方法但不保證成功。GC將對F-Queue中的對象進行第二次小規模的標記,然後回收

 

方法區回收(永久代):

兩部分內容:

回收廢棄常量與回收Java堆中的對象

廢棄常量:假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話,這個“abc”常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

無用類:該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;加載該類的ClassLoader已經被回收;該類對應的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

 

3.3.垃圾回收算法

最基礎的收集算法-標記清除法:算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。後續的收集算法都是基於這種思路並對其不足進行改進而得到的。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片

wKioL1mJVQqQmRE_AADmqeP3rec526.png-wh_50 

複製算法它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半

wKioL1mJVSeyQa8DAABrRd0Uxmk519.png-wh_50 

 

現在的商業虛擬機都採用這種收集算法來回收新生代,每次使用Eden和其中一塊Survivor,當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間

複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

 

標記-整理算法

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

wKioL1mJVUXyXDsVAACUqMdEJmM297.png-wh_50 

 

分代收集算法:

當前商業虛擬機的垃圾收集都採用“分代收集”(Generational Collection)算法,這種算法並沒有什麼新的思想,只是根據對象存活週期的不同將內存劃分爲幾塊。一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

3.4.垃圾回收器種類

總體圖(jdk1.7支持的垃圾收集器):

如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器還是老年代收集器

wKioL1mJVWHQoBPAAAE1oSWl9RA221.png-wh_50 

 

serial收集器單線程的收集器,它的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。是虛擬機運行在Client模式下的默認新生代收集器

wKioL1mJVYLzW3YmAAESSCqIYXc494.png-wh_50 

 

ParNew收集器Serial收集器的多線程版本,是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。

wKiom1mJVZ-xfQ7HAAEvh85WLT8390.png-wh_50 

Parallel Scavenge收集器:也經常稱爲“吞吐量優先”收集器,即是使用複製算法,又是並行的多線程收集器……看上去和ParNew都一樣,特點是它的關注點與其他收集器不同,目標則是達到一個可控制的吞吐量(Throughput),所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),也經常稱爲“吞吐量優先”收集器

 

 

參數:

-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間,停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導致垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。

GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。如果把此參數設置爲19,那允許的最大GC時間就佔總時間的5%(即1 /(1+19)),默認值爲99,就是允許最大1%(即1 /(1+99))的垃圾收集時間。

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

 

Serial Old收集器:

Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

wKioL1mJVbuTEJIXAAEYIIaPJnQ901.png-wh_50 

Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

 

CMS收集器收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。是基於“標記—清除”算法實現的。

 

wKioL1mJVdKSlPqJAAGEySC682Q226.png-wh_50 

CMS收集器對CPU資源非常敏感,CMS默認啓動的回收線程數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還分出一半的運算能力去執行收集器線程,

 

CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。

CMS是一款基於“標記—清除”算法實現的收集器,如果讀者對前面這種算法介紹還有印象的話,就可能想到這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)。

 

G1收集器當今收集器技術發展的最前沿成果之一,是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK 1.5中發佈的CMS收集器。與其他GC收集器相比,G1具備如下特點:並行與併發;分代收集;空間整合;可預測的停頓,但是出現時間較短,有待商榷

 

 

3.4.gc日誌

 33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]  

 100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 

GC日誌開頭的GC”Full GC”說明了這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的,例如下面這段新生代收集器ParNew的日誌也會出現Full GC”(這一般是因爲出現了分配擔保失敗之類的問題,所以才導致STW)。如果是調用System.gc()方法所觸發的收集,那麼在這裏將顯示Full GC (System)”接下來的DefNew”Tenured”Perm”表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名爲“Default New Generation”,所以顯示的是DefNew”。如果是ParNew收集器,新生代名稱就會爲ParNew”,意爲“Parallel New Generation”。如果採用Parallel Scavenge收集器,那它配套的新生代稱爲“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。後面方括號內部的“3324K->152K(3712K)”含義是“GC前該內存區域已使用容量-> GC後該內存區域已使用容量 (該內存區域總容量)”。而在方括號之外的“3324K->152K(11904K)”表示“GCJava堆已使用容量 -> GCJava堆已使用容量 (Java堆總容量)”

再往後,“0.0025925 secs”表示該內存區域GC所佔用的時間,單位是秒。

名稱對應關係:

 

 

3.5.垃圾收集器參數總結

wKiom1mJVfjRt-fiAAZLRXTXjE4964.png-wh_50 

 

wKiom1mJVhXAd2hBAAMHjNg3lHc345.png-wh_50 

 


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