視覺化Java垃圾回收的原理和實現

TP_funny發表於2015-01-20
基礎
當談到釋放不再使用的記憶體,垃圾回收已經在很大程度上取代了早期技術,比如手動記憶體管理和引用計數。
這是件好事,因為記憶體管理令人厭煩,學究式地簿記是計算機擅長的,而不是人擅長的。在這方面,語言的執行時環境比人強。
現代的垃圾回收非常高效,遠遠超過早期語言中典型的手工分配。通常,具有其它語言背景的人只盯著垃圾回收造成的中斷,卻沒有完全理解自動記憶體管理髮生作用的上下文環境。
標記&清除是Java(及其它執行時環境)用於垃圾回收的基本演算法。
在標記&清除演算法中,引用會從每個執行緒棧的楨指向程式的堆。所以,從棧開始,循著指標找到所有可能的引用,然後再循著這些引用遞迴下去。
當遞迴完成,就找到了所有的活物件,其它的都是垃圾。
請注意,人們經常漏掉的一點是,執行時環境本身也有一個“分配清單(allocation list)”,上面列出了指向每個物件的指標,該列表由垃圾回收器負責維護,並幫助垃圾回收器進行垃圾清理。因此,執行時環境總是可以找出由它建立但尚未回收的物件。

圖一
上面插圖中所示的棧只是一個與單個應用程式執行緒相關的棧;每個應用程式執行緒都有一個類似的棧,每個棧本身都有一組指向堆的指標。
如果垃圾回收器試圖在應用程式執行過程中獲取活物件的快照,那麼它就要追蹤運動著的目標,那樣很容易漏掉一些嚴重超時的物件分配,因而無法獲得一個準確的快照。因此,“Stop the World”是有必要的;也就是,停止應用程式執行緒足夠長的時間,以便捕獲活物件的快照。
下面是垃圾回收器必須遵循的兩條黃金法則:
  • 垃圾回收器必須回收所有的垃圾。
  • 垃圾回收器必須從不回收任何活物件。
但這兩條規則並不是對等的;如果違反了第二條規則,結果會使資料遭到破壞。
另一方面,如果違反了第一條規則,則會是另一種情況,系統並不總是能夠回收所有的垃圾,但最終會回收所有的垃圾,那麼這是可以接受的,而實際上,這是垃圾回收器的基本原理。

HotSpot
現在,我們來說下HotSpot,它實際上是一個C、C++以及許多特定於平臺的彙編程式組成的混合體。
當人們想到直譯器,就會想到一個很大的while迴圈,其中包含一個很長的switch語句。但HotSpot直譯器比那個要複雜的多(由於效能原因)。在開始閱讀JDK原始碼的時候,就會發現HotSpot中實在是有許多彙編程式程式碼。

物件建立
Java會預先分配大量的連續空間,就是我們所說的“堆”。之後,HotSpot完全在使用者空間裡管理這塊記憶體。
如果一個Java程式佔用了大量的系統(或核心)時間,那麼毫無疑問,它不是在進行垃圾回收——因為所有的垃圾回收記憶體“簿記(bookkeeping)”都是在使用者空間進行的。

記憶體池

圖二
“永久代(PermGen)”是一個儲存區域,用於儲存那些需要在程式生存期內一直存活的東西,如類的後設資料。不過,隨著應用程式伺服器的出現,它們有自己的類載入器,並且需要重新載入類的後設資料,永久代作為一個優化決策開始顯得糟糕,所幸,它在Java 8中消失了。
Java 8將會使用一個名為“元空間(Metaspace)”的新概念。元空間與永久代並不完全相同。它在堆的外面,由作業系統管理。這意味著,它不會在Java堆中,而是在本地記憶體裡。目前,這還不是一個非常好的訊息,因為沒有多少工具能夠讓使用者輕鬆地檢視本地記憶體。所以,永久代消失是件好事,但工具趕上這個變化還需要一些時間。

Java堆佈局
現在,我們來看下Java堆。注意堆空間之間的虛擬空間。它們提供了一點浮動量,以允許對記憶體池進行一定量的尺寸調整,又不用為任何物件移動付出代價。

圖三
“弱代假設(Weak Generational Hypothesis)”
就現狀而言,究竟為什麼要將堆分成所有這些記憶體池?

圖四
有的執行時事實無法通過靜態分析推匯出來。上面的插圖說明有兩組物件:一組存活時間短,一組存活時間長——所以,做額外的簿記以便利用這一事實是有意義的。在Java平臺中,有許多類似的作為優化寫入平臺的事實。

演示
Ben Evans進行了一系列的動畫演示。第一個演示是個Flash,說明了物件在Eden區和一個新生代Survivor空間之間移動,並最終進入老年代的過程。
圖五是用JavaFX再現了同樣的過程。

圖五

執行時開關
‘強制性’引數
  • -verbose:gc——為使用者輸出一些GC資訊
  • -Xloggc:<檔案路徑>——指定日誌輸出路徑,要確保磁碟有空間
  • -XX:+PringGCDetails——為輔助工具提供“最低限度資訊(Minimum information)”——用這個引數代替-verbose:gc
  • –XX:PrintTenuringDistribution——“過早提升(Premature promotion)”資訊

基本堆大小引數
  • -Xms<size> —— 設定預留給堆的最小記憶體值
  • -Xmx<size> —— 設定預留給堆的最大記憶體值
  • -XX:MaxPermSize=<size>——設定永久代的最大記憶體值——有利於Spring應用程式和應用伺服器
以前,我們被教導要把-Xms和-Xmx的值設的一樣大。不過這已經變了。因此,現在可以為-Xms設定一個合理範圍內較小的值,或者根本就不設定,因為堆的適應能力現在已經非常好了。

其它引數
  • -XX:NewRatio=N
  • -XX:NewSize=N
  • -XX:MaxNewSize=N
  • -XX:MaxHeapFreeRatio
  • -XX:MinHeapFreeRatio
  • -XX:SurvivorRatio=N
  • -XX:MaxTenuringThreshold=N

圖六

為什麼要有日誌檔案
日誌檔案的好處是能夠用於取證分析,可以使使用者免於為了再現問題而不得不再執行一次程式碼(如果是一個罕見的生產環境錯誤,那麼重現並不容易)。
另外,它們包含的資訊比針對記憶體的JMX MXBeans所能提供的資訊更多,且不說輪詢JMX本身會引入一系列GC問題。

工具
  • HP JMeter(用Google查詢一下)——免費,非常可靠,但不再提供支援/功能增強
  • GCViewer——免費,開源,但介面有點醜
  • GarbageCat——名字最好聽
  • IBM GCMV——支援J9
  • jClarity Censum——介面最美觀,而且最有用——不過,這是我們的偏見!

小結
  • 需要了解一些GC基礎理論
  • 要讓新生代的大部分物件在年輕時死亡
  • 開啟GC日誌!——原始日誌檔案難以閱讀——使用工具
  • 使用工具來幫助自己調優——測量,而不是猜測
來自:碼農網
相關閱讀
評論(1)

相關文章