概述
java中的垃圾回收器極大的提高了開發(fā)者的效率,但是垃圾回收器如果比較差可能會過多的消耗應用程序的資源。在 JVM performance optimization系列的第三篇文章中,Eva Andreasson為Java初學者提供了Java平臺內存模型和GC機制的概述。 隨后,她解釋了影響java程序性能的主要原因是“碎片”而不是GC,以及為什么分代垃圾收集和壓縮算法是目前Java應用程序中管理堆碎片的主要方式(盡管不是最新的)。
GC進程的目的是去釋放被占用的內存,這些內存再也不會被可達到對象所引用,GC進程是JVM動態(tài)內存管理系統(tǒng)的重要組成部分。在一個典型的垃圾收集周期中,所有可到達的對象(仍然被引用的對象)都被保留。被釋放的內存將會被分配給新的對象。
為了理解垃圾收集以及各種GC方法和算法,讀者必須首先了解Java的內存模型。
垃圾收集器和java內存模型
當你在命令行啟動應用程序的時候,如果去指定了啟動參數(shù)-Xmx (例如:ava -Xmx:2g MyApp),此時內存就被分配給java進程,這個內存被叫做java的堆內存或者直接叫做堆內存。這是一塊特殊的內存,所有被創(chuàng)建的對象都會被分配到這里。當Java程序繼續(xù)運行并且不停地分配新對象時,Java堆(即地址空間)將會被填滿。
最終,Java堆將被填滿,這意味著一個正在分配的線程無法分配給對象一個足夠大的連續(xù)的空閑內存段。這個時候JVM會通知GC去進行垃圾收集。java的 System.gc()語句會主動觸發(fā)GC。但是使用System.gc()并不保證垃圾收集是否會進行,在執(zhí)行GC之前首先要去確保是否能安全的啟動它,當應用程序的所有活動線程都處于允許的安全點時,啟動垃圾收集是安全的。例如當正在進行對象分配或者在執(zhí)行一系列優(yōu)化CPU指令的過程中,GC是不能執(zhí)行的,因為在這些情況下可能會破壞數(shù)據,從而影響最終的結果的正確性。
一個垃圾收集器不能去回收一個正在被引用的對象,這種行為是違反 JVM規(guī)范的。垃圾回收器也不需要去立刻回收已經死亡的對象,死亡對象最終會在接下來的垃圾收集周期中回收。盡管垃圾回收器有很多,但是都滿足以上兩點。垃圾收集的真正挑戰(zhàn)是識別出所有正在運行(仍然引用)的內存,并且回收任何未引用的內存,同時還要保證這樣做不會對運行中的應用程序造成不必要的影響。因此,垃圾收集器有兩個任務
- 快速的釋放未被使用的內存,以此來滿足應用程序的分配速率,以至于不會導致內存溢出。
- 回收內存的同時最小化地影響正在運行的應用程序的性能(例如,延遲和吞吐量)。
兩種類型的垃圾收集器
在本系列的第一篇文章中,我討論過垃圾收集的兩種主要方法,即引用計數(shù)和跟蹤收集器。在這篇文章中,我將深入研究每種方法,然后介紹一些用于在生產環(huán)境中實現(xiàn)跟蹤收集器的算法
引用計數(shù)收集器
引用計數(shù)收集器跟蹤每個Java對象的引用個數(shù),一旦數(shù)字變?yōu)?,這個對象所占據的內存會被立即釋放。引用計數(shù)收集器的最大的優(yōu)點就是會立刻回收內存。雖然保持一個未引用的內存開銷非常小,但是時刻保持引用計數(shù)是非常消耗性能的。
引用計數(shù)收集器最主要的困難是保證引用計數(shù)的準確性,以及處理圓形結構的復雜性。如果兩個對象互相引用但并沒有一個存活對象去指向它們,它們的引用計數(shù)永遠不可能為0,因此內存永遠不會被釋放。

回收與圓形結構有關的內存需要大量分析計算,這給算法帶來了昂貴的開銷,并因此降低了引用的性能。
跟蹤收集器
跟蹤收集器基于一種假設,通過迭代地跟蹤根對象集合(已知為存活對象的集合)尋找到根對象直接引用的對象,以及這些引用對象的后續(xù)引用對象,從而找到所有的存活對象。

在觸發(fā)垃圾收集時,通過分析寄存器、全局字段和堆棧幀找到根對象集合。當根對象集合初始化之后,跟蹤收集器會給這些對象排隊然后去標記為存活對象,并且跟蹤這些對象的后續(xù)引用。如果要標記所有找到的引用對象為存活對象,意味著已知的存活對象集合要隨著時間的推移而增加直至所有被引用的對象都被發(fā)現(xiàn)并且標記。一旦跟蹤收集器找到所有存活對象,它將回收剩余內存。
跟蹤收集器和引用計數(shù)收集器的區(qū)別在于它能夠處理圓形結構問題。對于大多數(shù)跟蹤收集器來說,最關鍵是標記階段,在能夠回收非引用內存之前等待一段時間。跟蹤收集器普遍用于動態(tài)語言的內存管理。目前為止它是java語言中最常用的并且已經在生產環(huán)境中進行了多年的商業(yè)驗證的垃圾收集器。接下來我將重點介紹跟蹤收集器有關的內容,首先介紹實現(xiàn)這種垃圾收集方法的一些算法。
跟蹤收集器算法
復制算法和標記清除算法不是新的算法,但它們仍然是目前實現(xiàn)跟蹤垃圾收集的兩種最常見算法。
復制算法
傳統(tǒng)的復制算法使用from-space 和 to-space, 這是在堆上單獨定義的兩塊地址空間,from-space上的存活對象會復制到to-space上,當from-space上的所有存活對象都被復制過去后,整個from-space將會被回收。然后從to-space的第一個空閑的位置開始進行對象分配
在以前,from-to算法的實現(xiàn)是當to-space滿了以后,GC再次啟動把to-space變成from-space ?,F(xiàn)在的復制算法的實現(xiàn)允許將堆中的任意地址空間分配為from-space 和 to-space。因此,它們不必相互交換位置。相反,它們都可以成為堆中的另一個地址空間。
復制收集器的一個優(yōu)點是對象被緊密地分配到空間中,完全的消除碎片,碎片是其他垃圾收集算法難以解決的一個常見問題。我將在本文后面討論一些內容
復制收集器的缺點
復制收集器經常會導致“全局停頓”,意味著,當GC執(zhí)行的時候,所有的應用程序都不能執(zhí)行。在處于全局停頓的時候,需要復制的區(qū)域越大,對應用程序性能的影響就越大,尤其是那種對時間特別敏感的應用程序。使用復制收集器必須要考慮最壞的情況,即所有的存活對象都在from-space里,你必須要保證有足夠的剩余空間去移動from-space,這就意味著to-space必須要足夠大,能讓to-space去容納from-space的所有東西。由于這個限制,復制算法的內存效率有點低。
標記清除收集器
部署在企業(yè)生產環(huán)境中的商業(yè)jvm大多數(shù)都運行標記-清除(或標記)收集器,使用這個收集器比復制收集器的對程序的性能影響更小。一些著名的標記清除收集器有CMS, G1, GenPar, 和 DeterministicGC
標記-清除收集器跟蹤引用,并把每個跟蹤到的對象標記為“存活”位。通常,一個集合位對應于一個地址,或者在某些情況下對應于堆上的一組地址?;顒游豢梢源鎯閷ο箢^中的位、位向量或位映射中的位。
當標記完成后將進入清除階段,如果收集器有一個清除階段,它基本上包括一些機制,用于再次遍歷堆(不僅是活動集,而且是整個堆長度),以定位所有未標記的連續(xù)內存地址空間塊。未被標記的內存將會被釋放并且回收。然后收集器將這些沒有標記的塊鏈接到有組織的空閑列表中。垃圾收集器中可以有各種空閑列表——通常按塊大小組織。一些jvm(如JRockit Real Time)使用啟發(fā)式實現(xiàn)收集器,啟發(fā)式根據應用程序分析數(shù)據和對象大小統(tǒng)計信息動態(tài)地確定大小范圍列表。如果應用程序內存消耗嚴重可以使用gc調優(yōu)選項,去適應各種應用程序場景和需求。在許多情況下,調優(yōu)至少可以幫助延緩這些階段對應用程序或服務水平協(xié)議(slas)的風險(SLA指定應用程序將滿足特定的應用程序響應時間,即,延遲)。但是,調整每次負載的改變和應用程序的修改都是一項重復性任務,因為調優(yōu)僅對特定工作負載和分配率有效。
標記-清除的實現(xiàn)
對于實現(xiàn)標記-清除收集,至少有兩種商業(yè)上可用且經過驗證的方法。一種是并行方法,另一種是并發(fā)(或大部分是并發(fā))方法
并發(fā)收集器
并行收集意味著分配給進程的資源被并行地用于垃圾收集。大多數(shù)商業(yè)上實現(xiàn)的并行收集器都會有“stop-the-world”——所有應用程序線程都停止,直到整個垃圾收集周期結束。在標記與掃描階段停止所有的線程以便于并行高效的使用資源。這將導致非常高的效率,通常會在諸如SPECjbb這樣的吞吐量基準測試中獲得高分。如果吞吐量對應用程序很重要,那么并行方法是一個很好的選擇。