java的垃圾收集器與內存分配策略
垃圾收集需要完成的三件事情
- 哪些內存需要回收
- 如何回收
- 什麼時候回收
垃圾收集針對的java內存區域
程序計數器、虛擬機棧、本地方法棧三個內存區域爲線程私有,線程結束時內存會回收,內存的分配和回收在編譯期能夠確定下來,所以不需要垃圾收集
java堆是線程共享的,在編譯期無法知道需要的內存大小,在運行期動態分配回收內存,所以垃圾收集針對的內存區域爲java堆。
哪些內存需要回收
判斷哪些內存需要回收是垃圾收集需要完成的第一件事。內存需要回收以爲着該片內存上的對象不再存活。
引用計數算法
Reference Counting 每個對象都有一個引用計數器,每當有地方引用它時,計數器數值+1,引用失效時,計數器數值-1.數值爲0時對象的內存需要回收。
引用計數算法無法避免循環引用的問題。
java使用的不是引用計數算法
可達性分析算法
在主流的商用程序語言中(java、c#,lisp)的實現中,都是通過可達性分析算法(Reachability Analysis)來判斷對象是否存活。
算法思路爲通過一些稱爲”GC Roots“的對象作爲起始點,遍歷搜索所有跟他們有關聯的對象。把每一個對象作爲一個節點,兩個對象有關聯時兩個節點間連線,對象與對象之間形成了圖的數據結構,從GC Roots出發遍歷這張圖,能遍歷到的對象爲可達對象,不能遍歷到的爲不可達對象。不可達的對象是不再存活,可回收的對象。
可達性分析算法可以解決循環引用的問題
java中,GC Roots對象包括以下幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(Native 方法)引用的對象
四種引用方式
判斷對象是否存活跟引用十分有關,在jdk1.2後,引用可以分爲四種方式
- 強引用(Strong Reference),強引用是程序中最常用的,如String str=“12”; Object obj=new Object(); 只要強引用還存在,垃圾收集器就不會回收該對象。內存不夠時會拋出outofmemory也不會回收存在強引用的對象
- 軟引用(Soft Reference,在內存不足,將要拋出內存溢出異常前,會把只跟軟引用關聯的對象列進回收範圍,如果回收後還是內存不足,才拋出內存溢出異常。
- 弱引用(Weak Reference)jvm進行垃圾回收時,無論內存是否足夠,都會回收只跟弱引用關聯的對象。
- 虛引用(Phantom Reference)一個對象是否存在虛引用,對它的存活時間不造成影響,無法通過虛引用獲取對應的對象。爲對象設置虛引用的目的在於該對象被垃圾回收時通過引用隊列收到一個系統通知
四種引用的詳細解釋與例子參考https://blog.csdn.net/linzhiqiang0316/article/details/88591907
如何回收
這一節討論不需要的內存如何回收,涉及內存回收算法,java中的分代收集算法和不同的垃圾收集器
內存回收算法
標記-清除算法(mark-sweep算法)
分爲標記和清除兩個階段,標記階段就是用可達性分析算法標記出所有不可達對象,清除階段則是把所有標記的內存清除掉。
標記-清除算法效率不高,且清除結束後會留下大量不連續的內存碎片,導致後續大對象分配內存時無法找到足夠連續的內存而不得不出發垃圾收集動作
複製算法
把內存劃分爲容量相同的兩塊,每次使用其中一塊,使用的一塊內存用完了,把還存活的對象複製到另一塊空閒內存,把之前使用的一塊內存直接清理掉。
複製算法的優點是不存在內存碎片問題,內存回收過程簡單高效,代價是每次只能會用一半內存,當存活對象較多時,把存活對象複製到空閒內存塊的代價較大。
標記-整理算法
標記過程與標記-清楚算法一致,但後續不是直接清理可回收對象的內存,而是把存活對象往一個方向移動,把存活對象壓在一個密集的區域,然後清理區域外的連續內存
分代收集算法
根據對象的存活週期把內存劃分爲幾塊,即把對象分爲幾代,然後可以根據每一代的特點採用不同的垃圾收集算法,這就是分代收集算法。
java的內存回收算法
java堆中按照分代收集算法一般分爲新生代和老年代。新生代又可以分爲Eden、From、To 三個區域 From和To統稱Survivor
- 新生代中每次垃圾收集都會有大量對象死去,只有少數對象存活,所以適合複製算法,複製算法內存按1:1劃分,是考慮到所有對象都存活的極端情況,此時複製的目的內存區域必須大於等於對象所在的內存區域。但研究表明新生代中98%的對象是“朝生夕死”的,所以上述所有對象存活的情況基本不會出現,基於此,HotSpot虛擬機把新生代的內存按8:1:1的比例劃分爲Eden和兩個Survivor
每次使用Eden和一個Survivor,使用的Survivor爲From,未使用的爲To,回收時,把Eden和From中的存活對象賦值到To中,然後清除Eden和From中的內存,隨後To從角色上變爲了From,From從角色上變爲了To,系統使用新的Eden和From分配內存。
基於此,HotSpot虛擬機中每次新生代可用的內存空間爲整個新生代內存空間的90%,考慮特殊情況,內存回收時存活對象多於整個新生代內存空間的10%,無法完全複製到To中,此時需要依賴其他內存(指老年代)進行分配擔保(Handle Promotion) - 老年代中對象存活率高,且沒有額外空間進行分配擔保,所以必須使用標記-清除算法或者標記-整理算法
對象何時進入老年代
- 大對象直接進入老年代
因爲新生代是使用的複製算法,所以要儘量減少複製的內存,所以對象內存到一定的值後就會直接進入老年代。通過參數-XX:PretenureSizeThreshold 可以設置對象直接進入老年代的大小閾值,超過設置值大小的對象直接進入老年代
PretenureSizeThreshold只對Serial和ParNew收集器有效 - 長期存活的對象進入老年代
每個對象會有一個Age的計數器,初始值爲0,在新生代中每經過一次minor GC並且存活(進入To區域一次),這個對象的Age就會加1,如果增加到一定程度(默認爲15)。那麼就會進入老年代中。
通過參數-XX:MaxTenuringThreshold可以設置進入老年代的age閾值 - 動態對象年齡判定
如果在新生代存活區中相同年齡所有對象大小的總和大於存活區的一半,年齡大於或等於該年齡的對象就會直接進入老年代。
比如現在存活區有三個對象,Age分別爲2、2、3。那麼Age爲3的這個對象就會進入老年代。
不同的垃圾收集器
此處討論的額垃圾收集器基於jdk1.7 update14之後的HotSpot虛擬機
兩個收集器間存在連線說明可以搭配使用
Serial收集器
最簡單,歷史最悠久的收集器,單線程運行,在進行垃圾收集時,必須停掉所有的工作線程,即“Stop The World”
與其它單線程收集器相比簡單而高效,適合用在單個CPU的環境
是虛擬機在client模式下的默認新生代收集器
ParNew收集器
多線程版本的Serial收集器
是虛擬機在Server模式下的首選新生代收集器,因爲除了Serial收集器,只有它能和CMS收集器(一款跨時代的併發收集器)配合工作。
ParNew收集器在單CPU下不會比Serial收集器效果好,在多CPU的條件下可以有效利用系統資源
Parallel scavenge收集器
多線程版本的新生代收集器,使用複製算法
Parallel scavenge收集器關注的是可控的吞吐量,而其他收集器關注縮短垃圾收集時用戶線程的停頓時間
Parallel scavenge收集器有一個參數-XX:+UseAdaptiveSizePolicy 打開以後虛擬機會根據運行情況動態調整參數提供最合適的停頓時間或者最大吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)
Serial Old收集器
Serial收集器的老年代版本。單線程,採用標記-整理算法
主要用於client模式下的虛擬機
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。
跟Parllel Scavenge收集器搭配使用稱爲“吞吐量優先”的收集器
CMS收集器
Concurrent Mark Sweep收集器以獲取最短回收停頓時間爲目標
CMS收集器有四個階段
- 初始標記
- 併發標記
- 重新標記
- 併發清除
初始標記和重新標記需要“Stop The World”
初始標記標記GC Roots能直接關聯到的對象
併發標記是進行GC Roots Tracing的過程
重新標記是爲了修正部分對象的標記,這些對象在併發標記階段因用戶程序繼續進行導致其標記發生變動
耗時最長的併發標記與併發清除可以與用戶程序併發進行
G1收集器
略過
什麼時候回收
這一節討論何時觸發GC。
GC分爲minor GC和Major GC(Full GC)
- minor GC 是發生在新生代的垃圾收集動作,發生較頻繁,觸發 minorGC的條件如下–新對象需要進入新生代內存區而Eden區的內存不足以存放新對象時觸發minor GC
- Major GC /Full GC 是發生在老年代的垃圾收集動作,經常但不一定會觸發至少一次的Minor GC,一般GC速度比Minor GC慢十倍。當對象從新生代或者直接進入老年代,但老年代所剩內存不足提供給對象,則會出發Major GC
程序中可以顯示調用System.gc 但調用時不一定會觸發GC,而能會在後面某個時間再進行GC,GC的觸發對於程序員來說其實是透明的,無法準確預知GC發生的時間
總結
本文討論了垃圾收集需要考慮的三件事情–哪些內存需要回收、如何回收和什麼時候回收,基於此討論垃圾收集的經典算法–可達性分析算法、標記-清除算法、複製算法、標記-整理算法和分代收集算法。其中在理論的基礎上對java中具體的的內存回收算法以及多個版本的垃圾收集器進行了介紹。