GC(Garbage Collect 垃圾回收)
GC分爲 Young區的 Minor GC,Old區的Major GC , Young區和Old區的Full GC。
首先說說運行時數據區的垃圾回收問題
程序計數器、虛擬機棧、本地方法棧這三個區域都是隨着線程創建/銷燬而生/死的。棧中的棧楨隨着方法的進入和退出而執行出棧和入棧操作的。每一個棧楨中分配多少內存基本上是在類結構確定下來事時就已知的,因此這三個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自己就回收了。而Java堆和方法區就不一樣了, 一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才知道會創建那些對象,這部分內存分配和回收都是動態的,垃圾收集器所關注的是這部分內存。
J方法區(或者HotSpot虛擬機中的永久代)進行垃圾收集的“性價比”一般比較低:在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收70%-95%的空間,而永久代的垃圾收集率遠低於此。
永久代的垃圾收集主要分爲兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量回收爲例,假如一個字符串“ABC”已經引入常量池中,但是當前系統沒有任何一個string對象是叫ABC的(沒有任何string對象引用常量池中的ABC常量),也沒有其他地方引用這個字面量,如果在這時候發生內存回收,而且有必要的話,這個ABC常量就會被系統“請”出常量池。常量池中的其他類、接口、方法、字段的符號引用也與此類似。
誰是垃圾對象?
兩種方式判斷對象是否是來及對象:
引用計數法
對於某個對象而言,只要應用程序中持有改對象的引用,就說明改對象不是垃圾,如果一個對象沒有任何指針對其引用,它就是垃圾了。
那如果AB相互持有引用,但是沒用對象引用AB,就會導致AB永遠也不會被回收?
所有這個引用計數法已經廢棄了!
可達性分析
通過GC Root的對象,開始向下尋找,看某個對象是否可達。
what? (一臉懵…)
這個算法的基本思想是通過一系列名爲“GC Root”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象那個刀片GC Root沒有任何引用鏈相連時,則證明此對象是不可用的,會被作爲垃圾回收掉。
GC Root是啥對象?
能作爲GCRoot:類加載器,Thread、虛擬機棧的本地變量表、static成員、常量引用、本地方法棧的變量
- 虛擬機棧(棧楨中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(一般說的Native方法)引用對象
引用
如果reference類型的數據中存儲的數值代表的是另一個塊內存中的起始地址,就稱這塊內存代表着一個引用。
引用分爲四種:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)- 強引用就是指在程序代碼之中普遍純在的,類似Object object = new Object()這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
- 軟引用是用來描述一些還有用但並非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行二次回收。如果這次回收了還沒有足夠的內存,纔會拋出內存溢出異常。
- 弱引用也就用來描述非必要對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
- 虛引用也稱爲幽靈引用或幻影引用,它是最弱的一種 引用,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象的實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被垃圾收集器回收時收到一個系統通知。
垃圾收集器算法
標記-清除(Mark-Sweep)
- 標記
找出內存中許需要回收的對象,並把它們標記出來。
此時堆中所有的對象都會被掃描一遍,從而才能確定需要回收的對象,比較耗時。
- 清除
清除掉被標記需要回收的對象,釋放出對應的內存空間
缺點
標記清除之後會產生大量不聯繫的內存碎片,空間碎片太多可能會導致以後在程序運行過程需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
- 標記和清除兩個過程都比較耗時,效率不高
- 會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次的垃圾收集動作。
複製算法
將內存劃分爲兩塊相等的區域,每次只使用其中一塊,如下
當其中一塊內存使用完了, 就將還存活的對象複製到另一塊上面,然後把已經使用過的內存空間一次清除掉。
缺點
空間利用率降低
標記-整理(Mark-Compact)
複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都有100%存活的極端情況,所以老年代一般不能直接選用這種算法。
標記過程仍然與“標記-清除”算法一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清除掉邊界以外的內存。
讓所有存活的對象向一端移動,清理掉邊界以外的內存。
分代收集算法
爲了增加垃圾回收的效率,JVM會根據對象存活週期的不同將內存分爲幾塊,堆中分爲新生代和老年代。
這樣可以根據各個年代的特點採用最適當的收集算法。
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。
而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記清除”或者“標記-整理”算法來進行回收。
Young區:複製算法(對象在被分配之後,可能生命週期比較短,young區複製率比較高)
Old區: 標記清除或標記整理(Old區對象存活時間比較長,複製來複制去沒必要費時間,不如做標記在清理)
垃圾收集器
Serial收集器
Serial是一種單線程收集器,不僅僅意味着它只會使用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是其在進行垃圾收集的時候需要暫停其他線程。
優點:簡單高效,擁有很高的單線程收集效率
缺點:收集過程需要暫停所有線程
算法:複製算法
適用範圍:新聲代
應用: client模式下的默認新生代收集器
ParNew收集器
可以吧這個收集器理解爲Serial收集器的多線程版本。多線程並行執行垃圾收集。
優點:在多CPU時,比Serial效率高。
缺點: 收集過程暫停所有應用線程,單CPU時比Serial效率差。
算法: 複製算法
適應範圍: 新生代
應用:運行在Serial模式下的虛擬機中首選中的新生代收集器
Parallel Scavenge 收集器
paraller Scavenge 收集器是一個新生代收集器,它也是適應複製算法的收集器,又是並行的多線程收集器,看上去和ParNew一樣,但是Parallel Scanvenge更關注系統的吞吐量 。
吞吐量 = 運行用戶代碼的時間 / 運行用戶代碼的時間 + 垃圾收集時間
比如虛擬機總共運行了100分鐘,垃圾收集時間用了1分鐘,吞吐量 =(100-1)/100 =99%
若吞吐量越大,意味着垃圾收集的時間越短,則用戶代碼可以充分利用CPU資源,儘快完成程序的運算任務。
-xx:MaxGCPauseMillis控制最大的垃圾收集停頓時間
-xx:GCRation 直接設置吞吐量大小
Serial Old收集器
Serial old收集器是Serial收集器的老年代版本,也是一個單線程收集器,不同的是採用“標記-整理算法”,運行過程和Serial收集器一樣。
parallel Old 收集器
Parallel Old收集器是Parallel Scavenger收集器的老年代版本,使用多線程和“標記-整理算法”進行垃圾回收。
吞吐量優先
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。
採用的是“標記-清除算法”,整個過程分爲4步。
- 初始標記 CMS initial mark 標記GC Root能關聯到的對象 Stop The World
- 併發標記 CMS concurrent mark 進行GC Roots Tracing
- 重新標記 CMS remark 修改併發標記因用戶程序變動的內容 Stop The world
- 併發清除 CMS concurrent sweep
由於整個過程中,併發標記和併發清除,收集器線程可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。
優點: 併發收集,地停頓
缺點: 產生大量空間碎片,併發階段會降低吞吐量
G1收集器
G1收集器在JDK 7正式作爲商用的收集器
與前幾個收集器相比,G1有以下特點
併發與並行
分代收集(仍然保留了分代的概念)
空間整合(整體上屬於“標記-整理”算法,不會導致空間碎片)
可預測的停頓(比CMS更先進的地方在於能讓使用這明確指定一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間 不的超過N毫秒)
使用G1收集器時,Java堆的內存佈局與其他收集器又很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,他們都是一部分Region(不需要鏈接)的集合。
工作過程可以分爲如下幾個步驟
- 初始標記(Initial Marking) 標記一下GC Roots能夠關聯的對象,並且修改TAMS的值,需要暫停用戶線程
- 併發標記(Concurrent Marking) 從 GC Roots 進行可達性分析,找出存活的對象,與用戶線程併發執行
- 最終標記 (Final Marking) 修正在併發標記階段因爲用戶程序的併發執行導致變動的數據,需暫停用戶線程
- 篩選回收 (LiveData Counting and Evacuation)對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間制定回收計劃
垃圾收集器分類
- 串行收集器 -> Serial 和 Serial Old
只能有一個垃圾回收線程執行,用戶線程暫停。使用於內存比較小的 嵌入式設備。
- 並行收集器[吞吐量優先] -> Parallel Scanvenge、Parallel Old
多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態,適用於科學計算,後臺處理等交互場景。
- 併發收集器 [停頓時間優先] -> CMS G1
用戶線程和垃圾收集線程同事執行(但並不一定是並行的,可能是交替執行),垃圾收集線程在執行的時候不會停頓用戶線程的運行,適用於相對時間有要求的場景 比如web
小小知識點
吞吐量和停頓時間
- 停頓時間:垃圾收集器 進行 垃圾回收終端應用執行響應的時間
- 吞吐量: 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)
停頓時間越短就越適合需要和用戶交互的程序,良好的相應速度能提升用戶的體驗
高吞吐則可以高效利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。
這兩個指標也是評價垃圾會收器好壞的標準,其實調優也就是觀察這兩個變量。
選擇什麼樣的垃圾收集器
- 優先調整堆的大小讓服務器自己來選擇
- 如果內存小於100M,使用串行收集器
- 如果是單核,並且沒有停頓時間要求,使用串行或JVM自己選
- 如果允許停頓時間超過1秒,選擇並行或JVM自己選
- 如果響應時間更重要,並且不能超過1秒,使用併發收集器
G1收集器,是否使用G1收集器
JDK 7開始使用,JDK 8非常成熟,JDK 9 默認的垃圾收集器。
- 50%以上的堆被存活對象佔用
- 對象分配和晉升的速度變化非常大
- 垃圾回收時間比較長
如何開啓需要的垃圾收集器
串行
- -XX: +UseSerialGC
- -XX: +UseSerialOldGC
並行(吞吐量優先)- -XX: +UseParallelGC
- -XX: +UseParallelOldGC
併發收集器(響應時間優先)- -XX: +UseConcMarkSweepGC
- -XX: +UseG1GC