概述
都知道Java是自動進行內存管理的,有自己的垃圾回收機制,那么具體Java是怎樣進行垃圾回收的呢?本章就來總結一下Java中的垃圾回收器與垃圾收集算法。
回收算法之前提:如何判斷對象需要進行回收
進行垃圾回收之前需要判斷哪些對象是不用的,是需要進行回收的。其中最簡單也是最耳熟能詳的判斷方法就是引用計數法。顧名思義,一個對象有一個地方引用,該對象引用計數器的值就加1,當不需要引用的時候計數器的值則減1,當計數器的值為0時則代表沒有別處引用該對象,該對象可以進行回收。然而這種方法正因為簡單所以存在比較明顯的缺點:無法回收互相引用的對象。即倆個對象互相引用,計數器的值均為1,然而當這兩個對象都是無用對象時卻無法對其進行回收,導致內存泄露。
第二個方法是大部分虛擬機所采用的可達性分析算法。主要原理是通過判斷對象引用鏈能否和虛擬機規定的GC Root對象連接。對象引用鏈就好像鏈表:a引用b,b引用c 當abc均沒有能和Root對象相連的引用鏈時,即可對abc進行回收。那么Root對象選取的標準是什么呢?首先必須是程序運行階段會一直存在的對象,不然你自己都需要垃圾回收還怎么判斷別人。。所以可以作為Root對象的包括以下幾種:
1.虛擬機棧中本地變量表引用的對象,注意這里并不是棧幀中的局部變量表,因為局部變量表在方法退出后隨著棧幀的銷毀而銷毀。
2.方法區(1.8改為元數據區)中類靜態屬性引用的對象
3.方法區中常量引用的對象
4.Native方法(非Java代碼的方法)引用的對象
總的來說不管是引用計數還是可達性分析都跟對象引用息息相關。接下來介紹Java中的幾種引用類型。Java中一共定義了4中類型的引用(不過平常用到的基本只有強引用),他們的引用強度由強到弱分別為:
1.強引用:即平常 new xxx()所生成的對象即為強引用。只要強引用存在,該對象就不會被回收。
2.軟引用:使用 SoftReference<T> 實現,他的使用和LIst差不多,使用 T t = soft.get() 得到存放在軟引用中的對象。當虛擬機沒有足夠內存,將要發生內存溢出時會對其進行回收,利用這一特性可以用來做內存緩存。定義一個Map,value為SoftReference<T>類型,實現一個自動管理的內存Map,不過該方式也不常用一般都是使用外部緩存的方式實現,例如Redis。
3.弱引用:WeakReference<T>實現,用法類似于軟引用,但被他引用的對象只能生存到下一次垃圾回收之前。
4.虛引用:PhantomReference<T>實現,調用虛引用的get()方法會返回null,所以木有辦法通過他獲取到一個對象實例。一般用于和ReferenceQueue一起使用完成對象垃圾回收前的對象清理工作,一個對象enqueue則代表該對象已經被GC了。
除了對象,沒有人引用的字符串常量和類也需要進行回收。判斷類是否能夠被回收的條件有三個:堆中已經沒有該類的實例,該類的CLassLoader已經被回收,該類的Class對象沒被引用。能夠卸載的類必須是自定義類加載器加載的類,而Java本身的三大類加載器加載的類是不能被卸載的,關于類加載器的問題以后在總結吧。。。。
垃圾回收算法
1. 標記清除:這個是比較簡單的回收算法,先對要回收的對象進行標記,然后進行統一刪除。缺點就是當存活的對象內存地址不是連續的時候會出現大量的內存碎片,導致分配大對象的時候沒有足夠的內存而不得不頻繁觸發垃圾回收。該算法是CMS垃圾收集器的基礎。
2. 復制算法:先進行標記操作,然后將存活的對象復制到另一塊內存上再對其在進行清除。因為新生代對象大部分都是活不長的。。所以新生代的回收算法一般采用復制算法。將Eden 和From Surviour中存活的對象復制到To Surviour。第二次回收的時候To區和Surviour會調換名字,直到To區的對象達到一定的條件晉升老年代。該算法被新生代垃圾回收器使用。
3. 標記整理:先進行標記操作,然后將存活的對象移到一起對剩下的內存進行統一清除,避免了標記清除的缺點。該算法被老年代垃圾回收器所使用。
垃圾收集器
1. Serial系列:單線程,意味著回收的時候不能進行用戶操作。
? ? 分為Serial新生代與Serial老年代。一般服務器應用好像沒人用。。所以了解一下就可以了
2. Parallel系列
? ? 分為Parallel新生代與Parallel老年代。是吞吐量優先的垃圾回收器。一般電腦上默認安裝的JDK就是使用了該收集器。
3. ParNew:?Serial新生代的多線程版本即多條線程同時進行垃圾回收,Tomcat默認的新生代回收器就是他。
4. CMS:Concurrent Mark Sweep(并發標記清除),是獲取最短回收停頓時間的收集器,用戶線程和回收線程可以同時工作。Tomcat默認老年代收集器就是他。
由于他是大部分服務端應用的主流回收器還是寫詳細些吧:回收主要分為4個階段,初始標記,并發標記,重新標記,并發清除。。每個階段干什么了看名字應該很清楚了。由于是和用戶線程并發運行,在回收的時候需要預留一部分內存給用戶線程,所以要設置老年代內存達到多大比例進行垃圾回收。
雖然他是主流的收集器,但有如下缺點:1.對cpu資源敏感,應用程序分配一部分cpu資源去進行垃圾收集導致用戶代碼響應速度變慢。2.無法處理并發清除時所產生的垃圾,只能留到下一次回收。3. 如上標記清除的缺點問題。CMS可以設置相應參數在FullGC時進行內存碎片的整理。
5. G1:可以進行垃圾回收時間的預測,設定回收時間不超過該值。不同于其他收集器的是他自己就可以管理新生代與老年代,不需要同其他回收器合作。他將整個堆劃分為多個Region,G1會跟蹤每個Region回收的價值,每次回收時根據價值列表與預測的回收時間回收相應Region。他的回收與CMS基本相同,分為初始標記,并發標記,最終標記與最終的篩選回收4個階段
6.ZGC:它是JDK 11中的垃圾回收器,是比G1更加優秀的垃圾回收器。ZGC承諾:回收的暫停時間不會超過10ms,暫停時間不會隨著堆容量的增加而增加,能夠處理從MB到TB大小的堆空間。ZGC主要有如下特點,并發執行,同G1一樣基于Region,能夠處理內存碎片問題,使用了“彩色指針”和加載屏障(這倆個術語我也不懂,這里就摘抄R大的一段話作為解釋【與標記對象的傳統算法相比,ZGC在指針上做標記,在訪問指針時加入Load Barrier(讀屏障),比如當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新為有效地址再返回,也就是,永遠只有單個對象讀取時有概率被減速,而不存在為了保持應用與GC一致而粗暴整體的Stop The World】)。
關于ZGC更詳細的介紹這里就貼一篇公眾號文章,江南白衣大大寫的,很贊,點我查看。
關于Minor GC,Major GC,FULL GC
這里就直接拷貝R大的一段回答吧。。寫的很好,受益匪淺。
作者:RednaxelaFX
針對HotSpot VM的實現,它里面的GC其實準確分類只有兩大種:
Partial GC:并不收集整個GC堆的模式
? ??Young GC:只收集young gen的GC
? ??Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
? ??Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。
Major GC通常是跟full GC是等價的,收集整個GC堆。但因為HotSpot VM發展了這么多年,外界對各種名詞的解讀已經完全混亂了,當有人說“major GC”的時候一定要問清楚他想要指的是上面的full GC還是old GC。
最簡單的分代式GC策略,按HotSpot VM的serial GC的實現來看,觸發條件是:
young GC:當young gen中的eden區分配滿的時候觸發。注意young GC中有部分存活對象會晉升到old gen,所以young GC后old gen的占用量通常會有所升高。
full GC:當準備要觸發一次young GC時,如果發現統計數據說之前young GC的平均晉升大小比目前old gen剩余的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,默認也是觸發full GC。
后記:關于對象晉升老年代的問題
最后說一下對象晉升的問題,什么情況下對象會晉升至老年代?
1. 大對象,當對象大于虛擬機參數設置的之時會直接進入老年代
2. 在新生代經歷了多次的GC(每一次GC對象年齡加1,默認為15)依然存活的對象直接進入老年代。
3. 若Surviour中相同年齡所有對象大小超過Surviour的一半,對象年齡大于等于該年齡的直接進入老年代。該行為稱作動態年齡判斷。