如何判斷一個對象是垃圾
我們都知道了當堆中的區域沒有足夠內存去存放對象時就會觸發垃圾回收,那麼如何來判斷一個對象是不是垃圾呢?
1.
引用計數法:
一旦相互持有引用,就導致對象永遠沒法被回收
例如:
給對象定義一個引用計數器,當該對象被引用時該計數器就++,引用結束時就–。那麼當垃圾回收時就會回收掉計數器爲0的對象。
那麼就有個問題,如果兩個本身是無用的對象相互引用,瞞天過海,瞞過JVM就會導致這兩個對象永遠不會被回收掉。
2.
可達性分析
由GC Root出發,開始尋找,看看某個對象是否可達
GC Root:可以是 類加載器、Thread、本地變量表、static成員、常用引用、本地方法棧中的變量等
例如:從GC root開始,被引用的就是可達,即不是無用對象不會被回收,反之就是垃圾對象,可以被回收。
垃圾回收算法
判斷一個對象已經是垃圾了,那麼它是怎麼被回收的呢?
1.
標記清除算法:
分爲標記和清除兩階段:首先標記出所有需要回收的對象,然後統一回收所有被標記的對象
缺點:
GC後會產生很多不連續的空間
如下圖:淺藍色就是存活的對象,灰色就是要被回收的對象,白色就是沒被使用的空間
垃圾回收之後,即變成如下圖所示:
顯然會產生很多不連續的空間,假設一個格子時1MB,新建一個對象爲4MB,按理說還有15MB的空間,但是沒有連續的4MB的空間,所以這個4MB的對象放不下就會提前觸發GC了,我們要知道GC次數是越少越好的。
2.
複製算法:
將可用內存按容量劃分爲大小相等的兩塊,每次只用其中一塊。當這塊內存用完了,就將還存活的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
相比於標記清除算法,解決了不連續空間碎片的問題
缺點:
將空間一分爲二,浪費一半的空間
如下圖:將空間一分爲二,綠色部分爲保留的部分,用來複制GC後存活的對象
GC後即變成如下圖所示:
存活的對象全部複製到右邊,左邊就變成了保留的部分,用來存放下次GC後存活的對象
3.
標記整理算法:
標記過程仍然與標記清除算法
一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
相比於上邊兩種算法,既解決了不連續空間碎片的問題,也解決了浪費一半空間的問題
GC後即變成如下圖所示:
分代收集算法
我們知道,堆內存中分爲不同的區域即,那麼針對不同的區域是不是要去使用不同的垃圾回收算法呢?肯定是的。
Young區:
採用複製算法
原因:
Young區的大部分對象都是朝生夕死的,只有少部分存活。那麼複製這個少部分對象的成本就會比較低。Young區中的Eden區和Survivor區都是採用複製算法。
old區:
採用標記清除或標記整理
原因:
因爲old區的對象相對於存活時間比較長了,如果用複製算法的話,這時候會有大量的對象存活得不到回收,是不是就意味着要複製大量的對象從而複製的成本會很高的。所以採用標記清除或標記整理的算法更合適。
垃圾收集器
有了上邊的垃圾回收算法了,是不是就要對其實現,那麼不同的垃圾收集器就是對不同垃圾回收算法的實現
(1)Serial
(2)Serial Old
(3)ParNew
(4)Parallel Scavenge
(5)Parallel Old
(6)CMS
(7)G1
垃圾收集器搭配使用
,如下圖:
上邊是適用於Young區,下邊是適用於old區,G1收集器比較特殊。
Serial垃圾收集器
實現了複製算法。如下圖,單線程,GC的時候應用程序會暫停,在jdk1.3之前是主流的收集器。與其搭配使用的可以是Serial Old、CMS。Serial Old與 Serial相似,都是單線程收集器
ParNew垃圾收集器
用於Young區,實現了複製算法。如下圖, 多線程,也會停止用戶應用程序的線程,對於Serial不同的是ParNew用多線程的方式來進行垃圾收集。與其搭配使用的可以是Serial Old、CMS。
Parallel Scavenge垃圾收集器
用於Young區,實現了複製算法。和ParNew垃圾收集器一樣,都是多線程的方式來進行垃圾收集,只不過Parallel Scavenge相比ParNew,更加關注吞吐量
。與其搭配使用的可以是Serial Old、Parallel Old 。
CMS垃圾收集器
用於old區,全稱Concurrent Mark Sweep
,一個併發收集器
,更加關注於停頓時間
。上邊所說的垃圾收集器,進行垃圾收集的時候,不管是單線程還是多線程,都需要停止用戶應用程序的線程來進行垃圾收集。而CMS可以與用戶線程並行來執行垃圾收集,如下圖:CMS的垃圾收集分爲四個階段:
1.初始標記
:單線程來進行初始標記,這個過程會非常非常的快所以採用單線程,通過初始標記來找到GCROOT能夠關聯到的對象。
2.併發標記
:多線程來進行標記,防止初始標記不完整,對GCROOT關聯進一步的追蹤。可以和用戶線程併發執行。
3.重新標記
:單線程來進行標記,爲了修正併發標記期間因用戶程序繼續動作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
4.併發清理
:與用戶線程併發進行,減少停頓時間。
G1垃圾收集器
適用於Young區和old區,這個收集器比較厲害了,是目前比較主流的收集器,這裏就作爲了解。如下圖:
它和CMS相似,更加關注停頓時間,與CMS不同的是,G1收集器用戶可以自定義設置一個預期的停頓時間,在進行篩選回收的時候,可以根據用戶設置的停頓時間來進行合理的回收。
相關知識總結
垃圾收集器分類
①串行收集器
->Serial和Serial Old->只能有一個垃圾回收線程執行,用戶線程停止。適用於內存比較小的嵌入式設備
。
②並行收集器[吞吐量優先]
->parallel Scanvenge和parallel Old->多條垃圾回收線程並行工作,用戶線程停止。適用於科學計算、後臺處理等交互場景。
③併發收集器[停頓時間優先]
->CMS、G1->用戶線程和垃圾收集線程同時進行(不一定是並行的,可能是交替執行),垃圾收集線程在執行的時候不會停止用戶線程。適用於相對時間有要求的場景,比如web。
吞吐量和停頓時間
這兩個指標也是評價垃圾收集器的標準,其實JVM調優也就是觀察這兩個變量。
吞吐量:
運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)
停頓時間:垃圾收集器進行垃圾回收時用戶應用程序的停頓時間
注意:
停頓時間越短就越適合需要和用戶交互的程序,良好的響應速度能提升用戶體驗;
高吞吐量則可以高效的利用cpu時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多的交互任務。
如何選擇合適的收集器
1.優先調整堆的大小,讓服務器自己選擇
2.如果內存小於100M,使用串行收集器
3.如果是單核,並且沒有停頓時間的要求,使用串行或者讓JVM自己選
4.如果允許停頓時間超過1秒,選擇並行或者JVM自己選
5.如果響應時間最重要,並且停頓時間不能超過1秒,使用併發收集器
如何開啓收集器
1.串行收集器
-xx:+UseSelialGC
-xx:+UseSelialOldGC
2.並行(吞吐量優先)
-xx:+UseParallelGC
-xx:+UseParallelOldGC
3.併發收集器(響應時間優先)
-xx:+UseConcMarkSweepGC
-xx:+UseG1GC