Java效能最佳化[3]:記憶體回收(GC)

來源:互聯網
上載者:User

  上次的文章講到參考型別和基本類型由於記憶體配置上的差異導致的效能問題。那麼今天就來聊一下和記憶體釋放(主要是GC)有關的話題。
  事先聲明一下:雖說SUN公司已經被Oracle吞併了,但是出於習慣,同時也為了偷懶節省打字,以下仍然稱之為SUN公司。

  ★JVM的記憶體
  在Java虛擬機器規範中(具體章節請看“這裡”),提及了如下幾種類型的記憶體空間:
  ◇棧記憶體(Stack):每個線程私人的。
  ◇堆記憶體(Heap):所有線程公用的。
  ◇方法區(Method Area):有點像以前常說的“進程程式碼片段”,這裡面存放了每個載入類的反射資訊、類函數的代碼、編譯時間常量等資訊。
  ◇原生方法棧(Native Method Stack):主要用於JNI中的原生代碼,平時很少涉及。

  關於棧記憶體(Stack)和堆記憶體(Heap),已經在上次的文章中掃盲過了,大伙兒應該有點印象。由於今天咱們要討論的“記憶體回收”話題,主要是和堆記憶體(Heap)有關。其它的幾個玩意兒不是今天討論的重點。等以後有空了,或許可以單獨聊一下。

  ★記憶體回收機制簡介
  其實Java虛擬機器規範中並未規定記憶體回收的相關細節。記憶體回收具體該怎麼搞,完全取決於各個JVM的設計者。所以,不同的JVM之間,GC的行為可能會有一定的差異。下面咱拿SUN官方的JVM來簡單介紹一下GC的機制。
  ◇啥時候進行記憶體回收?
  一般情況下,當JVM發現堆記憶體比較緊張、不太夠用時,它就會著手進行記憶體回收工作。但是大伙兒要認清這樣一個殘酷的事實:JVM進行GC的時間點是無法準確預知的。因為GC啟動的時刻會受到各種運行環境因素的影響,隨機性太大。
  雖說咱們無法準確預知,但如果你想知道每次記憶體回收執行的情況,還是蠻方便的。可以通過JVM的命令列參數“-XX:+PrintGC”把相關資訊列印出來。
  另外,調用System.gc()只是建議JVM進行GC。至於JVM到底會不會做,那就不好說啦。通常不建議自己手動調用System.gc(),還是讓JVM自行決定比較好。另外,使用JVM命令列參數“-XX:+DisableExplicitGC”可以讓System.gc()不起作用。
  ◇誰來負責記憶體回收?
  一般情況下,JVM會有一個或多個專門的記憶體回收線程,由它們負責清理回收垃圾記憶體。
  ◇如何發現垃圾對象?
  記憶體回收線程會從“根集(Root Set)”開始進行對象引用的遍曆。所謂的“根集”,就是正在啟動並執行線程中,可以訪問的引用變數的集合(比如所有線程當前函數的參數和局部變數、當前類的成員變數等等)。記憶體回收線程先找出被根集直接引用的所有對象(不妨叫集合1),然後再找出被集合1直接引用的所有對象(不妨叫集合2),然後再找出被集合2直接引用的所有對象......如此迴圈往複,直到把能遍曆到的對象都遍曆完。
  凡是從根集通過上述遍曆可以到達的對象,都稱為可達對象或有效對象;反之,則是不可達對象或失效對象(也就是垃圾)。
  ◇如何清理/回收垃圾?
  通過上述階段,就把垃圾對象都找出來。然後記憶體回收線程會進行相應的清理和回收工作,包括:把垃圾記憶體重新變為可用記憶體、進行記憶體的整理以消除記憶體片段、等等。這個過程會涉及到若干演算法,有興趣的同學可以參見“這裡”。限於篇幅,咱就不深入聊了。
  ◇分代
  早期的JVM是不採用分代技術的,所有被GC管理的對象都存放在同一個堆裡面。這麼做的缺點比較明顯:每次進行GC都要遍曆所有對象,開銷很大。其實大部分的對象生命週期都很短(短命對象),只有少數對象比較長壽;在這些短命對象中,又只有少數對象佔用的記憶體空間大;其它大量的短命對象都屬於小對象(很符合二八原理)。
  有鑒於此,從JDK 1.2之後,JVM開始使用分代的記憶體回收(Generational GarbageCollection)。JVM把GC相關的記憶體分為年老代(Tenured)和年輕代(Nursery)、持久代(Permanent,對應於JVM規範的方法區)。大部分對象在剛建立時,都位於年輕代。如果某對象經曆了幾輪GC還活著(大齡對象),就把它移到年老代。另外,如果某個對象在建立時比較大,可能就直接被丟到年老代。經過這種策略,使得年輕代總是儲存那些短命的小對象。在空間尺寸上,年輕代相對較小,而年老代相對較大。
  因為有了分代技術,JVM的GC也相應分為兩種:主要收集(Major Collection)和次要收集(Minor Collection)。主要收集同時清理年老代和年輕代,因此開銷很大,不常進行;次要收集僅僅清理年輕代,開銷很小,經常進行。

  ★GC對效能會有啥影響?
  剛才介紹了GC的大致原理,那GC對效能會造成哪些影響捏?主要有如下幾個方面:
  ◇造成當前運行線程的停頓
  早期的GC比較弱智。在它工作期間,所有其它的線程都被暫停(以免影響記憶體回收工作)。等到GC幹完活,其它線程再繼續運行。所以,早期JDK的GC一旦開始工作,整個程式就會陷入假死狀態,失去各種響應。
  經過這些年的技術改進(包括採用分代技術),從JDK 1.4開始,GC已經比較精明了。在它幹活期間,只是偶爾暫停一下其它線程的運行(從長時間假死變為暫時性休克)。
  ◇遍曆對象引用的開銷
  試想如果JVM中的對象很多,那遍曆完所有可達對象肯定是比較費勁的工作,這個開銷可不小。
  ◇清理和回收垃圾的開銷
  遍曆完對象引用之後,對垃圾的清理和回收也有較大的開銷。這部分開銷可能包括複製記憶體塊、更新對象引用等等。

  ★幾種收集器
  ◇兩個效能指標
  因為今天聊的是效能的話題,必然會提到衡量GC效能的兩個重要指標:輸送量(Throughput)和停頓時間(Pause Time)。輸送量這個詞不是很直觀,解釋一下:就是JVM不用於GC的時間佔總時間的比率。輸送量是越大越好,停頓時間是越小越好。
  不同的應用程式對這兩個指標的關注點不一樣(後面具體會說),也就是所謂的“眾口難調”。很多JVM廠商為了迎合“眾口”,不得不提供多種幾種垃圾收集器供使用者選擇。不同的收集器,採用的收集策略是不一樣的,下面具體介紹。
  ◇串列收集器(Serial Collector)
  使用命令列選項“-XX:+UseSerialGC”指定。
  這種收集器是最傳統的收集器。它使用單線程進行記憶體回收,對於單CPU機器比較合適。另外,小型應用或者對上述兩個指標沒有特殊要求的,可以使用串列收集器。
  ◇並行收集器(Parallel Throughput Collector)
  顧名思義,這種收集器使用多個線程進行記憶體回收以達到高輸送量。記憶體回收線程的數量通過命令列選項“-XX:ParallelGCThreads=n”指定。可以設定該數值以便充分利用多CPU/多核。
  當使用命令列選項“-XX:+UseParallelGC”時:它會針對年輕代使用多個記憶體回收線程,對年老代依然使用單個線程的串列方式。此選項最早在JDK 1.5引入。
  當使用命令列選項“-XX:+UseParallelOldGC”時:它針對年輕代和年老代都使用多個記憶體回收線程的方式。不過此選項從JDK 1.6才開始引入。
  ◇並發收集器(Concurrent Low Pause Collector)
  使用命令列選項“-XX:+UseConcMarkSweepGC”指定。
  這種收集器優先保證程式的響應。它會盡量讓記憶體回收線程和應用自身的線程同時運行,從而降低停頓時間。此選項從JDK 1.4.1開始支援。
  ◇增量收集器(Incremental Collector)
  自從JDK 1.4.2以來,SUN官方就停止維護該收集器了。所以俺就節省點口水,不多說了。

  ★如何降低GC的影響?
  ◇盡量減少堆記憶體的使用
  由於GC是針對儲存在堆記憶體的對象進行的。咱們如果在程式中減少引用對象的分配(也就相應降低堆記憶體配置),那對於提高GC的效能是很有協助滴。上次“字串過濾實戰”的文章給出了一個例子,示範了如何通過降低堆記憶體的分配次數來提升效能。

  ◇設定合適的堆記憶體大小
  JVM的堆記憶體是有講究的,不能太大也不能太小。如果堆記憶體太小,JVM老是感覺記憶體不夠用,可能會導致頻繁進行記憶體回收,影響了效能;如果堆記憶體太大,以至於作業系統的大部分實體記憶體都被JVM自個兒霸佔了,那可能會影響其它應用程式甚至作業系統本身的效能。
  另外,年輕代的大小(或者說年輕代與年老代的比值)對於GC的效能也有明顯影響。如果年輕代太小,可能導致次要收集很頻繁;如果年輕代太大,導致次要收集的停頓很明顯。
  JVM提供了若干和堆記憶體大小相關的命令列選項,具體如下:
------------------------------
-Xms  設定初始堆記憶體
-Xmx  設定最大堆記憶體
-Xmn  設定年輕代的大小
-XX:NewRatio=n  設定年輕代與年老代的比例為“n”
-XX:NewSize=n  設定年輕代大小為“n”
------------------------------
  一般情況下,JVM的預設參數值已經夠用。所以沒事兒別輕易動用上述選項。如果你非調整不可,一定要做深入的效能對比測試,保證調整後的效能確實優於預設參數值。

  ◇輸送量和停頓的取捨
  前面提到了不同應用的眾口難調。常見的口味有兩種:(1)看重輸送量,對停頓時間無所謂;(2)側重於停頓時間。
  對於某些在背景、單純運算密集型的應用,屬於第一種。比如某些科學計算的應用。這時候建議使用並行收集器。
  對於涉及使用者UI互動的、即時性要求比較高、程式需要快速響應的,屬於第二種。比如某些案頭遊戲、某些電信交換系統。這時候建議使用並發收集器。

  ★相關的參考資料
  ◇GC調優資料
  SUN官方提供了若干關於JVM記憶體回收調優的說明文檔,JDK 1.4.2請看“這裡”;JDK 1.5請看“這裡”;JDK 1.6請看“這裡”。
  ◇JVM命令列選項說明
  這是SUN公司內的某個有心人整理的各種命令列參數大全,在“這裡”。包括有每個參數所適用的JDK版本。
  ◇虛擬機器規範
  “這裡”是SUN官方的JVM規範。

著作權聲明
本部落格所有的原創文章,作者皆保留著作權。轉載必須包含本聲明,保持本文完整,並以超連結形式註明作者編程隨想和本文原始地址:

http://program-think.blogspot.com/2009/04/java-performance-tuning-3-gc.html

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.