垃圾收集算法
引用計數
堆中的每個對象都有一個引用計數,當對象被引用時引用計數加1,當對象的引用被重新賦值或超出有效區域時引用計數減1,當一個對象被回收后,它所引用的對象的引用計算減1。當一個對象的引用計數變為0時就被回收。
引用計數的優點:
垃圾收集器可以很快地執行,當一個對象的引用數為0時就可以回收這個對象,垃圾收集交織在程序的正常執行過程中,不用長時間中斷程序的正常執行。
引用計數的缺點:
- 每次引用計數的增加和減少會帶來額外的開銷
- 無法檢測出循環引用
根搜索算法
垃圾檢測通過建立一個根對象的集合(局部變量、棧楨中的操作數,在本地方法中引用的對象,常量池等)并檢查從這些根對象開始的可觸及性來實現。根對象總是可訪問的,如果存在根對象到一個對象的引用路徑,那么稱這個對象是可觸及的或活動對象,否則是不可觸及的,不可觸及的對象就是垃圾對象。
標記清除
分為標記和清除兩個階段,在標記階段,垃圾收集器跟蹤從根對象的引用,在追蹤的過程中對遇到的對象打一個標記,最終未被標記的對象就是垃圾對象,在清除階段,回收垃圾對象占用的內存。可以在對象本身添加跟蹤標記,也可以用一個獨立的位圖來設置標記。
標記清除法是基礎的收集算法,其他算法大多時針對這個算法缺點的改進。
有兩個缺點:
- 效率
- 存在內存碎片
復制算法
將內存劃分為大小相等的兩個區域,每次只使用其中的一個區域,當這個區域的內存用完了,就將可觸及的對象直接復制到新的區域并連續存放以消除內存碎片,當可觸及對象復制完后,清除舊內存區域,修改引用的值。
這種算法的缺點很明顯,可使用內存變為了原來的一半,太過浪費。
一般情況下,新生代中的對象大多生命周期很短,也就是說當進行垃圾收集時,大部分對象都是垃圾,只有一小部分對象會存活下來,所以只要保留一小部分內存保存存活下來的對象就行了,用不著使用一半的內存。在新生代中一般將內存劃分為三個部分:一個較大的Eden空間和兩個較小的Survior空間(一樣大小),每次使用Eden和一個Survior的內存,進行垃圾收集時將Eden和使用的Survior中的存活的對象復制到另一個Survior空間中,然后清除這兩個空間的內存,下次使用Eden和另一個Survior,HotSpot中默認將這三個空間的比例劃分為8:1:1,這樣被浪費掉的空間就只有總內存的1/10了。
這樣的內存空間劃分是基于這樣一種假設,即每次垃圾收集時大部分對象都是垃圾,只有少部分對象存活。如果遇到例外的情況怎么辦,在某次垃圾收集時存活下來的對象超過了預留的那個Survior空間的總大小,這就需要依賴其他的內存進行分配擔保了(參考分代收集,前面的描述中也說了這是新生代中的方法)
標記整理
普通的標記清除會在內存中留下內存碎片,復制算法如果不想浪費掉50%內存就需要有內存分配擔保,一般是內存分代,但總有一代是沒有其他代為它擔保的。標記整理算法中標記的過程同標記清理一樣,但整理部分不是直接清除掉垃圾對象,而是將活動對象統一移動一內存的一端,然后清除邊界外的內存區域,這樣就避免了內存碎片。也不會浪費內存,不需要其他內存進行擔保
分代收集
大多數程序中創建的大部分對象生命周期都很短,而且會有一小部分生命周期長的對象,為了克服復制收集器中每次垃圾收集都要拷貝所有的活動對象的缺點,將內存劃分為不同的區域,更多地收集短生命周期所在的內存區域,當對象經歷一定次數的垃圾收集存活時,提升它的存在的區域。一般是劃分為新生代和老年代。新生代又劃分為Eden區,From Survior區和To Survior區。
自適應收集器
監聽堆中的情形,并且對應地調用合適的垃圾收集技術。
垃圾收集器
Serial
一個單線程的收集器,在進行垃圾收集時會暫停其他線程的工作,不適合用到Server端的虛擬機,但Client模式的模擬機還是可以用的,因為Client模式下的應用分配到的系統內存一般不大,垃圾收集可以很快完成。優點就是簡單高效,沒有線程交互開銷,可以獲得最高的單線程收集效率。
ParNew
Seria的多線程版本,可以多個線程收集垃圾,但如果CPU只有一核且沒有超線程,效果就不一定比Serial好了,如果是多核或有超線程,可以保證效果好于Serial,除Seria之外,這是唯一能與CMS收集器配合的垃圾收集器
Parallel Scavenge
使用復制算法的新生代多線程垃圾收集器,Parallel Scavenge收集器的關注點和其他收集器不同,其他收集器的關注點是盡可能縮短垃圾收集時用戶線程等待的時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput),即CPU用于運行用戶代碼的時間與CPU總消耗時間的比值。以縮短用戶線程等待時間的收集器適合用于需要與用戶交互的程序,而以吞吐量為目標的收集器適合用于不需要和用戶太多的交互,以后臺運算為目標的任務。
Parallel Scavenge可以通過參數設置每次垃圾收集需要停頓的時間和吞吐量目標,但停頓時間并不是越小越好,這是以犧牲吞吐量和新生代空間為代價的,因為要使垃圾收集停頓時間縮小,只能進行少量多次收集,或減小需要收集的空間大小。
還有一個-XX:UseAdaptiveSizePolicy參數,指定這個參數后,就不需要手工指定新生代的大小、Eden區和Survior區的比例大小和晉升老年代對象年齡等細節參數了,虛擬機會根據收集到的信息動態調整這些參數,這稱為自適應策略。
Serial Old
Serial的老年代版本,單線程收集器,使用"標記-整理"算法,主要被Client模式下的虛擬機使用,當被使用在Server模式時主要有兩個用途:
- 與Parallel Scavenge配合使用
- 作為CMS收集失敗時的備選方案。
Parallel Old
Parallel Scavenge的老年代版本,使用"標記-整理"算法,JDK1.6后提供的,在此之前,如果新生代選擇了Parallel Scavenge,老年代只能選擇Serial Old,由于Serial Old是單線程的垃圾收集器,可能會影響收集性能。Parallel Old出現后,就可以分別在新生代和老年代選擇Parallel Scavenge和Parallel Old組合了。
CMS(Concurrent Mark Sweep)
以獲取最短回收停頓時間為目標的收集器,使用“標記-清除”算法,整個回收過程分為以下4步:
- 初始標記(CMS Initial Mark)
- 并發標記(CMS Current Mark)
- 重新標記(CMS Remark)
- 并發清楚(CMS Concurrent Sweep)
初始標記與重新標記階段仍會暫停用戶線程的運行。
初始標記只是記錄下GC Root能直接關聯到的對象,速度很快。
并發標記就是GC Roots Tracing了,速度較慢,但可以和用戶線程同時運行。
重新標記是修正并發標記時由于用戶線程運行導致的標記記錄變動,這個階段會使用戶線程停頓,停頓時間比初始標記略長,但仍小于重新標記。
并發清除就是清除垃圾對象了,耗時較長,但可與用戶線程同時工作。
CMS的缺點
- 對CPU資源敏感,并發階段和用戶線程同時運行,影響服務器的響應速度,尤其是CPU核心數少時
- 無法處理浮動垃圾,由于并發階段用戶線程同時在運行,可能會在垃圾收集過程中產生新的垃圾,CMS無法處理這部分浮動垃圾,由于在進行垃圾收集時用戶線程同時在運行,需要額外的內存空間,所以不能等到內存滿時再進行GC,需要預留一部分空間,如果預留的這部分空間不夠GC時用戶線程創建新對象使用,就會使用預備方法,使用Serial Old進行一次Full GC。
- CMS基于“標記-清除”算法,進行垃圾回收后會存在內存碎片,當申請大的連續內存時可能內存不足,此時需要進行一次Full GC,可以通過參數指定進行Full GC后或進行多少次Full GC后進行一次內存壓縮來整理內存碎片。
G1(Garbage First)
基于"標記-整理"算法,避免了內存碎片的問題,并可精確地控制垃圾回收時的停頓。
G1收集器可以實現基本不犧牲吞吐量的前提下完成低停頓的內存回收,不同于之前的垃圾回收器,G1收集器的回收區域不是整個新生代或老年代,而是將整個Java堆劃分為多個固定大小的區域,并跟蹤這些區域里的垃圾堆積程度,在后臺維護一個優先列表,優先回收垃圾最多的區域。區域的劃分使每次回收時間變短,而優先級的劃分使得每次回收的區域可以回收最多的垃圾,這就使用G1收集器可以在有限的時間內獲取最高的收集效率。
內存分配與回收策略
對像優先在新生代Eden區分配,當Eden區沒有足夠的內存時會發生一次Minor GC(新生代GC,Major GC或Full GC是老年代GC)
大對象可以直接在老年代分配內存,可以通過參數指定一個大小,大于這個大小的對象直接在老年代中分配內存。
進行Minor GC時,Eden區和一個Survior區中存活的對象會被復制到另一個Survior區,一個對象每在一次Minor GC中存活下來一次后這個對象的年齡就加1,當這個對象的年齡大于一定值(默認15)就會進入老年代。
如果Survior中相同年齡的對象占用的空間大于Survior空間的一半,那么年齡大于或等于這個年齡的對象會直接進入老年代,而不用等到達到特定年齡
當進行Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果小于,判斷是否開啟了HandlerPromotionFailure允許擔保失敗,如果開啟了就只進行Minor GC,否則進行Full GC。由于使用的之前Minor GC時的平均大小,如果某一次突然大小變大,導致老年代剩余空間不夠,即擔保失敗,會再進行一次Full GC。
finalize
GC時會對活動對象進行標記,沒有被標記的對象就是垃圾對象,但垃圾對象不會直接被清除,垃圾收集器還會判斷是否需要執行對象的finalize方法,如果對象沒有覆寫finalize方法或它的finalize已經被執行過一次,那么是沒有必要執行的,否則就認為是有必要執行的,當被判斷為有必要執行時,這個對象會被放入一個F-Queue隊列中,由一個后臺的低優先級的Finalizer線程執行隊列中的對象的finalize方法,對象可以在這個方法中中復活自己,即重新被其他對象引用,但這個函數只會被垃圾收集器運行一下,第二次回收這個對象時這個函數不會再被調用。稍后GC會對F-Queue隊列中的對象執行第二次標記。
文章列表