1. 程式人生 > >成為Java GC專家(1):深入淺出Java垃圾回收機制

成為Java GC專家(1):深入淺出Java垃圾回收機制

  對於Java開發人員來說,瞭解垃圾回收機制(GC)有哪些好處呢?首先可以滿足作為一名軟體工程師的求知慾,其次,深入瞭解GC如何工作可以幫你寫出更好的Java應用。

  這僅僅代表我個人的意見,但我堅信一個精通GC的人往往是一個好的Java開發者。如果你對GC的處理過程感興趣,說明你已經具備較大規模應用的開發經驗。如果你曾經想過如何正確的選擇GC演算法,那意味著你已經完全理解你所開發的應用的特點。當然,我們不能以偏概全,這不能作為評價一個好的開發人員的共通標準。但是,我要說的是,深入理解GC是成為一名偉大的程式設計師的必經之路。

  這是成為JavaGC專家系列文章的第一篇,本篇主要針對GC機制進行介紹,在下一篇中,我們將重點探討分析GC狀態以及來自NHN的GC調優的例子(譯註:NHN是韓國第一大網際網路公司,業務包括搜尋引擎、遊戲、門戶等等。Cubrid開源資料庫的提供者,中國子公司

http://www.nhncorp.cn/,本文來自cubrid.org上的部落格文章)。

  本文的目的是以一種簡單的方式向你介紹GC機制。我希望這些文章能夠幫到你。實際上,我的學生已經在Twitter上釋出了一些很好的關於Java核心的文章,並且大受歡迎。有興趣的話,你也可以關注他們。

  回到正題,咱們繼續談垃圾回收,在學習GC之前,你首先應該記住一個單詞:“stop-the-world”。Stop-the-world會在任何一種GC演算法中發生。Stop-the-world意味著 JVM 因為要執行GC而停止了應用程式的執行。當Stop-the-world發生時,除了GC所需的執行緒以外,所有執行緒都處於等待狀態,直到GC任務完成。GC優化很多時候就是指減少Stop-the-world發生的時間。

按代的垃圾回收機制

  在Java程式中不能顯式地分配和登出記憶體。有些人把相關的物件設定為null或者呼叫System.gc()來試圖顯式地清理記憶體。設定為null至少沒什麼壞處,但是呼叫System.gc()會顯著地影響系統性能,必須徹底杜絕(還好,我還沒有見到NHN的哪個開發者呼叫這個方法)。

  在Java中,開發人員無法直接在程式程式碼中清理記憶體,而是由垃圾回收器自動尋找不必要的垃圾物件,並且清理掉他們。垃圾回收器會在下面兩種假設(hypotheses)成立的情況下被建立(稱之為假設不如改為推測(suppositions)或者前提(preconditions))。

  • 大多數物件會很快變得不可達
  • 只有很少的由老物件(建立時間較長的物件)指向新生物件的引用

  這些假設我們稱之為弱年代假設weak generational hypothesis)。為了強化這一假設,HotSpot虛擬機器將其物理上劃分為兩個–新生代(young generation)和老年代(old generation)。
  新生代(Young generation): 絕大多數最新被建立的物件會被分配到這裡,由於大部分物件在建立後會很快變得不可到達,所以很多物件被建立在新生代,然後消失。物件從這個區域消失的過程我們稱之為”minor GC“。

  老年代(Old generation): 物件沒有變得不可達,並且從新生代中存活下來,會被拷貝到這裡。其所佔用的空間要比新生代多。也正由於其相對較大的空間,發生在老年代上的GC要比新生代少得多。物件從老年代中消失的過程,我們稱之為”major GC“(或者”full GC“)

  請看下面這個圖表。

圖1 : GC 空間 & 資料流

  上圖中的持久代( permanent generation )也被稱為方法區method area)。他用來儲存類常量以及字串常量。因此,這個區域不是用來永久的儲存那些從老年代存活下來的物件。這個區域也可能發生GC。並且發生在這個區域上的GC事件也會被算為major GC。

  有些人可能會問:
  如果老年代的物件需要引用一個新生代的物件,會發生什麼呢?
  為了解決這個問題,老年代中存在一個”card table“,他是一個512 byte大小的塊。所有老年代的物件指向新生代物件的引用都會被記錄在這個表中。當針對新生代執行GC的時候,只需要查詢card table來決定是否可以被收集,而不用查詢整個老年代。這個card table由一個write barrier來管理。write barrier給GC帶來了很大的效能提升,雖然由此可能帶來一些開銷,但GC的整體時間被顯著的減少。

圖 2: Card Table 結構

新生代的構成

  為了更好地理解GC,我們現在來學習新生代,新生代是用來儲存那些第一次被建立的物件,他可以被分為三個空間

  •  一個伊甸園空間(Eden
  •  兩個倖存者空間(Survivor

  一共有三個空間,其中包含兩個倖存者空間。每個空間的執行順序如下:

  1. 絕大多數剛剛被建立的物件會存放在伊甸園空間。
  2. 在伊甸園空間執行了第一次GC之後,存活的物件被移動到其中一個倖存者空間。
  3.   此後,在伊甸園空間執行GC之後,存活的物件會被堆積在同一個倖存者空間。
  4.  當一個倖存者空間飽和,還在存活的物件會被移動到另一個倖存者空間。之後會清空已經飽和的那個倖存者空間。
  5. 在以上的步驟中重複幾次依然存活的物件,就會被移動到老年代。

  如果你仔細觀察這些步驟就會發現,其中一個倖存者空間必須保持是空的。如果兩個倖存者空間都有資料,或者兩個空間都是空的,那一定標誌著你的系統出現了某種錯誤。
  通過頻繁的minor GC將資料移動到老年代的過程可以用下圖來描述:


圖 3: GC執行前後對比

  需要注意的是HotSpot虛擬機器使用了兩種技術來加快記憶體分配。他們分別是是”bump-the-pointer“和“TLABs(Thread-Local Allocation Buffers)”。

  Bump-the-pointer技術跟蹤在伊甸園空間建立的最後一個物件。這個物件會被放在伊甸園空間的頂部。如果之後再需要建立物件,只需要檢查伊甸園空間是否有足夠的剩餘空間。如果有足夠的空間,物件就會被建立在伊甸園空間,並且被放置在頂部。這樣以來,每次建立新的物件時,只需要檢查最後被建立的物件。這將極大地加快記憶體分配速度。但是,如果我們在多執行緒的情況下,事情將截然不同。如果想要以執行緒安全的方式以多執行緒在伊甸園空間儲存物件,不可避免的需要加鎖,而這將極大地的影響效能。TLABs 是HotSpot虛擬機器針對這一問題的解決方案。該方案為每一個執行緒在伊甸園空間分配一塊獨享的空間,這樣每個執行緒只訪問他們自己的TLAB空間,再與bump-the-pointer技術結合可以在不加鎖的情況下分配記憶體。
  以上是針對新生代空間GC技術的簡要介紹,你不需要刻意記住我剛剛提到的兩種技術。不知道他們不會對你產生什麼影響,但是請務必記住在物件剛剛被建立之後,是儲存在伊甸園空間的。那些長期存活的物件會經由倖存者空間轉存在老年代空間。

老年代GC處理機制

  老年代空間的GC事件基本上是在空間已滿時發生,執行的過程根據GC型別不同而不同,因此,瞭解不同的GC型別將有助於你理解本節的內容。
  JDK7一共有5種GC型別:

  1. Serial GC
  2. Parallel GC
  3. Parallel Old GC (Parallel Compacting GC)
  4. Concurrent Mark & Sweep GC  (or “CMS”)
  5. Garbage First (G1) GC

  其中,Serial GC不應該被用在伺服器上。這種GC型別在單核CPU的桌面電腦時代就存在了。使用Serial GC會顯著的降低應用的效能指標。
  現在,讓我們共同學習每一種GC型別。

1. Serial GC (-XX:+UseSerialGC)

  新生代空間的GC方式我們在前面已經介紹過了,在老年代空間中的GC採取稱之為”mark-sweep-compact“的演算法。

  1. 演算法的第一步是標記老年代中依然存活物件。(標記)
  2. 第二步,從頭開始檢查堆記憶體空間,並且只留下依然倖存的物件。(清理)

  最後一步,從頭開始,順序地填滿堆記憶體空間,並且將對記憶體空間分成兩部分:一個儲存著物件,另一個空著(壓縮)。

2. Parallel GC (-XX:+UseParallelGC)


圖 4: Serial GC 與 Parallel GC的區別

  從上圖中,你可以輕易地看出serial GC和parallel GC的區別,serial GC只使用一個執行緒執行GC,而parallel GC使用多個執行緒,因此parallel GC更高效。這種GC在記憶體充足以及多核的情況下會很有用,因此我們也稱之為”throughput GC“。

3. Parallel Old GC(-XX:+UseParallelOldGC)

  Parallel Old GC在JDK5之後出現。與parallel GC相比,唯一的區別在於針對老年代的GC演算法。Parallel Old GC分為三步:標記-彙總-壓縮(mark – summary – compaction)。彙總(summary)步驟與清理(sweep)的不同之處在於,其將依然倖存的物件分發到GC預先處理好的不同區域,演算法相對清理來說略微複雜一點。

4. CMS GC (-XX:+UseConcMarkSweepGC)

圖 5: Serial GC & CMS GC

  就像你從上圖看到的那樣, CMS GC比我之前解釋的各種演算法都要複雜很多。第一步初始化標記(initial mark) 比較簡單。這一步驟只是查詢那些距離類載入器最近的倖存物件。因此,停頓的時間非常短暫。在之後的並行標記(concurrent mark )步驟,所有被倖存物件引用的物件會被確認是否已經被追蹤和校驗。這一步的不同之處在於,在標記的過程中,其他的執行緒依然在執行。在重新標記(remark)步驟,會再次檢查那些在並行標記步驟中增加或者刪除的與倖存物件引用的物件。最後,在並行交換(concurrent sweep )步驟,轉交垃圾回收過程處理。垃圾回收工作會在其他執行緒的執行過程中展開。一旦採取了這種GC型別,由GC導致的暫停時間會極其短暫。CMS GC也被稱為低延遲GC。它經常被用在那些對於響應時間要求十分苛刻的應用之上。

  當然,這種GC型別在擁有stop-the-world時間很短的優點的同時,也有如下缺點:

  •  它會比其他GC型別佔用更多的記憶體和CPU
  •  預設情況下不支援壓縮步驟

  在使用這個GC型別之前你需要慎重考慮。如果因為記憶體碎片過多而導致壓縮任務不得不執行,那麼stop-the-world的時間要比其他任何GC型別都長,你需要考慮壓縮任務的發生頻率以及執行時間。

5. G1 GC

  最後,我們來學習垃圾回收優先(G1)GC型別。


圖 6:  G1 GC的結構

  如果你想要理解G1,首先你要忘記你所學過的新生代和老年代的概念。正如你在上圖所看到的,每個物件被分配到不同的格子,隨後GC執行。當一個區域裝滿之後,物件被分配到另一個區域,並執行GC。這中間不再有從新生代移動到老年代的三個步驟。這個型別是為了替代CMS GC而被建立的,因為CMS GC在長時間持續運作時會產生很多問題。

  G1最大的好處是效能,他比我們在上面討論過的任何一種GC都要快。但是在JDK 6中,他還只是一個早期試用版本。在JDK7之後才由官方正式釋出。就我個人看來,NHN在將JDK 7正式投入商用之前需要很長的一段測試期(至少一年)。因此你可能需要再等一段時間。並且,我也聽過幾次使用了JDK 6中的G1而導致Java虛擬機器宕機的事件。請耐心的等到它更穩定吧。

  下一次我將討論GC優化相關的問題,但是在此之前我要先明確一件事情,假如應用中建立的所有物件的大小和型別都是統一的,那麼公司使用的WAS(譯註:Web Application Server)的GC引數可以是相同的。但是WAS所建立物件的大小和生命週期根據服務以及硬體的不同而不同。換句話說,不能因為某個應用使用的GC引數“A”,就說明同樣的引數也能給其他服務帶來最佳的效果。而是要因地制宜,有的放矢。我們需要找到適合每個WAS執行緒的引數,並且持續的監控和優化每個裝置上的WAS例項。這並不是我的一家之談,而是負責Oracle Java虛擬機器研發的工程師在 JavaOne 2010上已經討論過的。

  本文中我們簡略的介紹了Java的GC機制,請繼續關於我們的後續文章,我們將會討論如何監控Java GC狀態以及優化GC。

  另外,我特別推薦一本2011年12月釋出的《Java效能》(Amazon,也可以通過safari線上閱讀),還有在Oracle官網釋出的白皮書《JavaHotSpotTM虛擬機器記憶體管理》(這本書與Java效能優化不是同一本)

  作者:Sangmin Lee, NHN公司,效能工程師實驗室高階工程師。