J2SE 5.0-memory management whitepaper
1.垃圾回收器期職責
- 開闢空間
- 任何引用可達的物件都在記憶體內
- 回收不再使用的記憶體
3.垃圾回收器概念
3.1.垃圾回收器期望的效能
- 垃圾回收器必須安全,存活的物件不應該被釋放,應該釋放的物件存活的時間應該在很小的回收迴圈內.
- 垃圾回收器必須高效,不應該讓應用長時間停頓.根據三個方面來衡量,時間,空間,頻率.
堆太小,容易被填滿,垃圾回收蒐集的頻率高,回收速度快。
堆太大,不容易填滿,垃圾回收的頻率低,回收速度慢。
- 生成的空間碎片少.
- 可伸縮,垃圾回收器不應該成為應用程式瓶頸.
3.2.可選的設計方案
1.序列回收器 VS 並行回收器
序列回收器不能更好的利用多核CPU,只能使用一個核.
並行回收器可以把任務拆分,更好的利用多個核完成任務.但是會增加實現的複雜性和記憶體碎片.
2.併發 VS 停止應用程式
停止應用程式型別的垃圾回收器工作時,讓應用程式停止,垃圾回收工作完畢後應用程式繼續執行.
併發型垃圾蒐集器也需要讓應用程式停頓一小會.停止應用程式型別的垃圾回收器實現比並發回收器簡單,因為它工作時.
堆內容固定不變,併發回收器讓應用停止時間比停止應用程式型別回收器,但是它需要考慮在回收時應用程式同時更新的物件,
同樣,它需要大的堆記憶體.
3.壓縮 VS 非壓縮 VS 複製
在垃圾回收器確定哪些物件是垃圾時,此時可以壓縮記憶體,移動所有存活的物件然後重新計算剩餘記憶體大小.在壓縮後,更容易更快為物件開闢記憶體空間.
對於非壓縮行為,回收器直接在物件原始位置釋放記憶體,不移動所有存活物件,然後重新計算剩餘記憶體大小.但是非壓縮回收器能更快的完畢回收工作,副效應就是
帶來較多的記憶體碎片.通常,在釋放物件的原始位置開闢需要的空間比在壓縮後的空間中開闢要昂貴.它需要搜尋整個記憶體空間,直到找到一塊連續並且大小合適的記憶體.
第三種可替換的蒐集器是複製蒐集器,它複製存活物件至一塊不同的記憶體區域.好處就是,源記憶體可以直接當做是空記憶體,能夠更快的用於下次開闢空間,但是副效應就是
複製物件需要時間,還需要其他的記憶體空間.
3.3.測量效能
吞吐量:未花費在垃圾回收上的時間百分比,需要長期考慮. (not use in gc)/t
垃圾回收時間:和吞吐量相反,所有花費在垃圾回收上的時間. (use in gc)/t
停頓時間:發生垃圾回收時應用程式停頓時間.
垃圾回收頻率:多久發生一次垃圾回收,與正在執行的應用程式相關.
佔用的空間:可測量的大小,比如堆大小.
速度:成為垃圾的物件到物件佔用的空間變得可用之間的時間.
3.4.按代蒐集
使用按代蒐集時,記憶體區域被劃分成多個代.比較廣泛的分代方法是,對於年輕物件放一個代,年老的物件放一個代.
對於不同的代使用不同回收演算法.它們依據以下理論.
1.許多物件不會長時間被引用,在年輕時它們就會死掉.
2.年老物件很少引用年輕物件.
年輕代垃圾回收器發生回收行為頻繁,回收行為高效並且快,因為年輕代通常比較小,包含許多沒有被引用的物件.
年輕代中的物件經過多次回收行為,如果還活著,就轉移到年老代,年老代的記憶體通常大於年輕代,並且增加速度比年輕代慢.
所以,對於年老代的回收行為不頻繁,但是需要長時間才能完成.
為年輕代選擇垃圾回收器主要考慮的費用是速度,因為年輕代蒐集行為頻繁.另外來說,年老代回收演算法在空間管理上更加高效,因為年老代記憶體大小
比年輕代大,而且年老代演算法需要在垃圾密度很低的情況下工作的很好.
5.J2SE5.0 HotSpot JVM中垃圾回收器
J2SE5.0 HotSpot中所有回收器都是按代的回收器.這個章節描述回收器型別和代,然後討論為什麼為物件開闢空間速度很快,很高效.然後提供更多關於回收器的詳細資訊.
5.1.HotSpot中物件的代
HotSpot虛擬機器中的代包括年輕代,年老代,持久代.許多物件在年輕代中開闢和初始化.年老代中儲存經過多次回收後還倖存的物件,當然一些大的物件可能之間在
年老代中開闢空間.持久代儲存那些查詢方便的物件(用於JVM垃圾蒐集),比如物件和方法描述資訊,同樣也包括類和方法本身.
年輕代由Eden區域和倆塊小的倖存區組成,如圖2所示。大多數物件在Eden中開闢空間然後初始化.(正如上面提到的,少數物件可能直接在年老代開闢空間)。倖存區儲存年輕代中至少經歷過一次回收的物件,但是在變成年老代之前就會死掉的物件.在一個任意的給定的時間點,這些物件儲存在倖存區中的某一個區域內(圖中指示的是From區),倖存區中的未儲存物件的另外一塊區域儲存為空直到下一次垃圾蒐集.
5.2.垃圾蒐集器型別
當年輕代被填滿時,年輕代蒐集器在年輕代上執行(有適合也叫做minor collection).當年老代或者持久代被填滿時,執行一次full collection(有時候也叫做major collection).在這種
情況下,所有的代都被蒐集,通常,年輕代首先被收集.使用針對年代的演算法蒐集垃圾,因為這個演算法在年輕代中標識垃圾具有很高的效率.然後在年老代和持久代上執行特定的垃圾蒐集器.如果指定了壓縮,每個代都被單獨的壓縮.
如果年輕代先蒐集.有時候會造成年輕代提升太多的物件到年老代,導致年老代太過於飽和.在這種情況下,所有的蒐集器(除了CMS蒐集器)在年輕代上都不執行.
相反,年老代垃圾蒐集演算法在所有代上使用(CMS年老代演算法是比較特殊,因為它不能蒐集年輕代)
5.3.快速開闢空間
正如你在以下看到的那樣,在許多情況下都有一大片連續的記憶體塊用於給物件開闢記憶體,從這樣塊開闢空間的速度非常高效,使用一種比較簡單的技術,名字叫做bump-the-pointer,先前開闢的最後一個物件通常需要儲存起來.當一個新的開闢空間請求需要被滿足時,需要檢查代中剩餘的空間是否對申請合適,如果合適,更新最後一個物件,然後物件初始化.
對於多執行緒應用程式,開闢空間操作需要多執行緒安全.如果使用一個全域性鎖來保證安全,在代中開闢空間的行為會成為系統瓶頸,然後拉低效能.相反,HotSpot JVM採用一個叫做
Thread-Local Alloction Buffers(TLABs)的技術.它能提升多執行緒開闢空間的吞吐量(每個執行緒在自己的buffer裡面開闢空間,buffer是代的一小部分).在每個TLAB裡面既然只有一個執行緒開闢空間,此時可以使用bump-the-pointer來快速開闢空間,而不需要偶外的鎖.線上程填滿自己的TLAB的時候需要獲取一個新的TLAB,這種是發生的頻率很低,但是需要同步實現.多種技術由於使用TLAB讓空間浪費變成很小.比如,平均下來,TLABs可以讓記憶體分配器浪費的記憶體小於Eden的1%.使用TLABs+bump-the-pointer技術可以讓開闢非常高效,僅需要差不多10條原生的指令.
5.4.序列蒐集器
使用序列蒐集器,年輕代蒐集和年老代蒐集都是序列的(使用一個CPU).使用stop-the-world風格.這樣,當垃圾蒐集器進行時應用程式執行被強制停止.
在年輕代中使用序列蒐集器
圖3指出來在年輕代中使用序列蒐集器.Eden記憶體活的物件被複制到倖存區內(圖內標記為To的那塊),除了那些太大而不適合To空間的物件,這些物件直接被複制到年老代.
在From區域記憶體活的物件同樣被複制到其他區域(To),如果物件太老,直接複製到年老代.(注意:如果To空間已經滿了,從Eden或者From來的存活物件會被直接複製到年老代
,此時不管此物件在年輕代蒐集器下倖存過多少次).在Eden或者From區域內未被複制走的物件都是未活著的物件,它們不需要被再次測試是否存活.(圖內標記成X的那些物件)
在年輕代蒐集器完成後,所有Eden和倖存區內某一塊全是空的(倖存區內另外一塊空間儲存著存活的物件).此時,倖存區內倆個空間交換.具體檢視圖4.
在年老代中使用序列蒐集器
在年老代和持久代中序列蒐集器使用mark-sweep-compact演算法.在標記階段,蒐集器標示那些還存活的物件,在掃除階段,清理區域中所有的垃圾,然後蒐集器執行滑動壓縮.
滑動所有存活物件區去年老代開始地方(在持久代中一樣),與其相對的地方都是連續空閒的塊.具體檢視圖5.壓縮使得以後的開闢行為使用bump-the-pointer開闢空間很快.
何時使用序列蒐集器
執行在client-style的虛擬機器適合使用序列垃圾蒐集器,並且應用程式不需要低停頓的時間.在當前的硬體環境下,序列蒐集器能夠高效的管理許多隊空間為64MB的應用程式
並且全量蒐集最壞情況下的停頓時間小於半秒.
使用序列蒐集器
在J2SE5.0的發行版中,非伺服器類機器上預設選擇序列垃圾蒐集器,正如Section 5內描述的那樣.在其他機器上,可以通過指定-XX:UseSerialGC命令列引數來顯式指定.