一、如何確定一個對象是垃圾?
想要進行垃圾回收,得先知道什麼樣的對象是垃圾。
1.1 引用計數法
對於某個對象而言,只要應用程序中持有該對象的引用,就說明該對象不是垃圾。
如果一個對象沒有任何指針對其引用,它就是垃圾。
弊端:如果AB相互持有引用,就會導致用於不能回收。
1.2 可達性分析
通過一系列名爲“GC Roots”的對象作爲起始點,從“GC Roots”對象開始向下搜索,如果一個對象到“GC Roots”沒有任何引用鏈相連,說明此對象可以被回收。
能作爲GC Root:類加載器、Thread、虛擬機的本地變量表、static成員、常量引用、本地方法棧的變量等。
二、垃圾收集算法
已經能確定一個對象爲垃圾之後接下來要考慮的就是如何回收。
2.1 標記-清除(Mark-Sweep)
標記清除是最基礎的收集算法,是因爲後續的收集算法都是基於這種思路並對其缺點進行改進而得到的。
找出內存中需要回收的對象,並且把它們標記出來。
此時堆中所有的對象都會被掃描一遍,從而才能確定需要回收的對象,比較耗時。
清除掉被標記需要回收的對象,釋放出對應的內存空間。
缺點:
1)標記和清除兩個過程都比較耗時,效率不高
2)標記清除之後會產生大量不連續的內存碎片,內存碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,
無法找到足夠的連續內存,導致不得不提前觸發另一次垃圾收集動作。
2.2 複製(Copying)
複製的算法,將內存劃分爲兩塊相等的區域,每次只使用其中一塊,
當這一塊的內存使用完了,就將還存活的對象複製到另外一塊上面,然後把已經使用過的內存空間一次清除掉。
優點:不會產生內存碎片
缺點:這種算法的代價將內存縮小爲原來的一半,空間利用率降低。
對象存活率較高時就要執行更多的複製操作,效率將會變低。
2.3 標記-整理(Mark-Compact)
標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,
而是讓所有存活對象都向一端移動,然後直接清理掉邊界外的內存。
2.4 分代收集
對於之前說的三種垃圾收集算法,堆內存到底應該採用哪一種?
Young區:複製算法。每次GC都有大批對象死去,只有少量存活,因此採用複製算法效率高
Old區:標記清除或標記整理。Old區對象存活時間較長,如果用複製則複製量更多,沒有必要,不如做個標記再清理。
三、垃圾收集器
如果說收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現了。
3.1 Serial收集器
Serial收集器是最基本、發展歷史最悠久的收集器,在JDK1.3.1之前是虛擬機新生代收集的唯一選擇。
Serial收集器是一種單線程收集器,不僅意味着它只會使用一個CPU或者一條收集線程去完成垃圾收集工作,
更重要的是它在進行垃圾收集時需要暫停其他線程。
優點:簡單高效,有很高的單線程收集效率。
缺點:收集過程中需要暫停其他線程。
算法:複製算法
適用範圍:新生代
應用:Client模式下的默認新生代收集器
3.2 ParNew收集器
可以把這個收集器理解爲Serial收集器的多線程版本。
優點:CPU多時,比Serial效率高。
缺點:收集過程暫停所有應用程序線程,單CPU時比Serial效率差。
算法:複製算法
使用範圍:新生代
應用運行在Server模式下的虛擬機中首選的新生代收集器
3.3 Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器。又是並行的多線程收集器,
看上去和ParNew一樣,但是Parallel Scanvenge更關注 系統的吞吐量。
吞吐量=運行用戶代碼的時間/(運行用戶代碼的時間+垃圾收集的時間)
比如虛擬機一共運行了100分鐘,垃圾收集時間用了1分鐘,吞吐量=99/100=99%
吞吐量越大,意味着垃圾收集的時間越短,用戶代碼利用CPU資源的效率更高。
-XX:MaxGCPauseMillis 設制最大的垃圾收集停頓時間。
-XX:GCTimeRatio 直接設置吞吐量的大小。
3.4 Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一個單線程收集器,不同的是採用“標記-整理”算法,運行過程和Serial收集器一樣。
3.5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法進行垃圾回收。
吞吐量優先。
3.6 CMS收集器
CMS收集器是一種以實現最短回收停頓時間爲目標的收集器。
採用的是“標記-清除算法”,過程中分爲4步:
(1)初始標記 標記GC Roots能關聯到的對象 Stop The World-->速度很快
(2)併發標記 進行GC Roots Tracing ,不會Stop The World
(3)重新標記 修改併發標記因用戶程序變動的內容 Stop The World
(4)併發清除 CMS concurrent sweep
由於整個過程中,併發標記和併發清除,收集器線程可以和用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是和用戶線程一起併發地執行的。
優點:併發收集、低停頓
缺點:產生大量空間碎片、併發階段會降低吞吐量
3.7 G1收集器
G1收集器的特點:
並行與併發
分代收集(仍然保留了分代的概念)
空間整合(整體上屬於“標記-整理”算法,不會導致空間碎片)
可預測的停頓(比CMS更先進的地方在於,能讓使用者明確指定一個長度爲M毫秒的時間片段,消耗在垃圾收集上的時間不得超過N毫秒)
使用G1收集器時,Java堆的內存佈局就與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region的集合。
G1的工作過程可以分爲以下幾步:
初始標記 標記GC Roots能夠關聯的對象。(需要暫停用戶線程)
併發標記 從GC Roots進行可達性分析,找出存活的對象。(與用戶線程併發執行)
最終標記 修正在併發標記階段,因爲用戶程序的併發執行導致變動的數據。(需要暫停用戶線程)
篩選回收 對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間制定回收計劃
3.8 垃圾收集器的分類和適用
串行收集器-->Serial和Serial Old
只能有一個垃圾回收線程執行,用戶線程暫停。(適用於內存比較小的嵌入式設備)。
並行收集器[吞吐量優先]-->Parallel Scanvenge、Parallel Old
多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。(適用於科學計算、後臺處理等弱交互場景)
併發收集器[停頓時間]-->CMS、G1
用戶線程和垃圾收集線程同時執行(但並不是一定並行的,可能是交替執行的),垃圾收集線程在執行的時候不會停頓用戶線程的運行。(適用於對停頓時間有要求的場景,比如web)。
3.9 吞吐量與停頓時間
停頓時間->>垃圾收集器進行垃圾回收導致用戶線程停頓的時間
吞吐量->>運行用戶代碼的時間/(運行用戶代碼+垃圾收集時間)
停頓時間越短就越適合需要和用戶交互的程序,更快的響應速度提升用戶體驗;
高吞吐量則可以高效的利用CPU,儘快完成程序的運算任務,適合在後臺運算但不需要太多交互的任務。
其實調優也是這兩個指標更加符合應用的需要。
3.10 如何選擇合適的垃圾收集器
優先調整堆的大小,讓服務器自己來選擇
如果內存小於100M,使用串行收集器
如果是單核,而且沒有停頓時間的要求,使用串行或JVM自己選擇
如果運行停頓時間超過有1秒,選擇並行或JVM自己選
如果想要時間最重要,而且不能超過1秒,使用併發收集器
注意:這裏並行指的是GC線程同時執行,用戶線程會停頓。
併發指的是用戶線程和GC線程同時執行,用戶線程不停頓。
3.11 再看G1
G1收集器從JDK7開始使用,JDK8非常成熟,JDK9默認的垃圾收集器,適用於新老生代。
什麼情況需要使用G1收集器?
1)50%以上的堆被存活對象佔用
2)對象分配和晉升老年代的速度辯護非常大
3)垃圾回收時間比較長
3.12 開啓需要的垃圾收集器的參數
1)串行
-XX:+UseSerialGC
-XX:+UseSerialOldGC
2)並行(吞吐量優先):
-XX:+UseParallelGC
-XX:+UseParallelOldGC
3)併發收集器(響應時間優先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC