1. JVM內存管理:深刻垃圾收集器與內存分配策略html
http://www.iteye.com/topic/802638java
Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裏面的人卻想出來。
概述:
提及垃圾收集(Garbage Collection,下文簡稱GC),大部分人都把這項技術當作Java語言的伴生產物。事實上GC的歷史遠遠比Java來得久遠,在1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期,人們就在思考GC須要完成的3件事情:哪些內存須要回收?何時回收?怎麼樣回收?
通過半個世紀的發展,目前的內存分配策略與垃圾回收技術已經至關成熟,一切看起來都進入「自動化」的時代,那爲何咱們還要去了解GC和內存分配?答案很簡單:當須要排查各類內存溢出、泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術有必要的監控、調節手段。
把時間從1960年撥回如今,回到咱們熟悉的Java語言。本文第一章中介紹了Java內存運行時區域的各個部分,其中程序計數器、VM棧、本地方法棧三個區域隨線程而生,隨線程而滅;棧中的幀隨着方法進入、退出而有條不紊的進行着出棧入棧操做;每個幀中分配多少內存基本上是在Class文件生成時就已知的(可能會由JIT動態晚期編譯進行一些優化,但大致上能夠認爲是編譯期可知的),所以這幾個區域的內存分配和回收具有很高的肯定性,所以在這幾個區域不須要過多考慮回收的問題。而Java堆和方法區(包括運行時常量池)則不同,咱們必須等到程序實際運行期間才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,咱們本文後續討論中的「內存」分配與回收僅僅指這一部份內存。
對象已死?
在堆裏面存放着Java世界中幾乎全部的對象,在回收前首先要肯定這些對象之中哪些還在存活,哪些已經「死去」了,即不可能再被任何途徑使用的對象。
引用計數算法(Reference Counting)
最初的想法,也是不少教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,當有一個地方引用它,計數器加1,當引用失效,計數器減1,任什麼時候刻計數器爲0的對象就是不可能再被使用的。
客觀的說,引用計數算法實現簡單,斷定效率很高,在大部分狀況下它都是一個不錯的算法,但引用計數算法沒法解決對象循環引用的問題。舉個簡單的例子:對象A和B分別有字段b、a,令A.b=B和B.a=A,除此以外這2個對象再無任何引用,那實際上這2個對象已經不可能再被訪問,可是引用計數算法卻沒法回收他們。
根搜索算法(GC Roots Tracing)
在實際生產的語言中(Java、C#、甚至包括前面提到的Lisp),都是使用根搜索算法斷定對象是否存活。算法基本思路就是經過一系列的稱爲「GC Roots」的點做爲起始進行向下搜索,當一個對象到GC Roots沒有任何引用鏈(Reference Chain)相連,則證實此對象是不可用的。在Java語言中,GC Roots包括:
1.在VM棧(幀中的本地變量)中的引用
2.方法區中的靜態引用
3.JNI(即通常說的Native方法)中的引用
生存仍是死亡?
斷定一個對象死亡,至少經歷兩次標記過程:若是對象在進行根搜索後,發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記,並在稍後執行他的finalize()方法(若是它有的話)。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這點是必須的,不然一個對象在finalize()方法執行緩慢,甚至有死循環什麼的將會很容易致使整個系統崩潰。finalize()方法是對象最後一次逃脫死亡命運的機會,稍後GC將進行第二次規模稍小的標記,若是在finalize()中對象成功拯救本身(只要從新創建到GC Roots的鏈接便可,譬如把本身賦值到某個引用上),那在第二次標記時它將被移除出「即將回收」的集合,若是對象這時候尚未逃脫,那基本上它就真的離死不遠了。
須要特別說明的是,這裏對finalize()方法的描述可能帶點悲情的藝術加工,並不表明筆者鼓勵你們去使用這個方法來拯救對象。相反,筆者建議你們儘可能避免使用它,這個不是C/C++裏面的析構函數,它運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。須要關閉外部資源之類的事情,基本上它能作的使用try-finally能夠作的更好。
關於方法區
方法區即後文提到的永久代,不少人認爲永久代是沒有GC的,《Java虛擬機規範》中確實說過能夠不要求虛擬機在這區實現GC,並且這區GC的「性價比」通常比較低:在堆中,尤爲是在新生代,常規應用進行一次GC能夠通常能夠回收70%~95%的空間,而永久代的GC效率遠小於此。雖然VM Spec不要求,但當前生產中的商業JVM都有實現永久代的GC,主要回收兩部份內容:廢棄常量與無用類。這兩點回收思想與Java堆中的對象回收很相似,都是搜索是否存在引用,常量的相對很簡單,與對象相似的斷定便可。而類的回收則比較苛刻,須要知足下面3個條件:
1.該類全部的實例都已經被GC,也就是JVM中不存在該Class的任何實例。
2.加載該類的ClassLoader已經被GC。
3.該類對應的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方經過反射訪問該類的方法。
是否對類進行回收可以使用-XX:+ClassUnloading參數進行控制,還可使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載、卸載信息。
在大量使用反射、動態代理、CGLib等bytecode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要JVM具有類卸載的支持以保證永久代不會溢出。
垃圾收集算法
在這節裏不打算大量討論算法實現,只是簡單的介紹一下基本思想以及發展過程。最基礎的蒐集算法是「標記-清除算法」(Mark-Sweep),如它的名字同樣,算法分層「標記」和「清除」兩個階段,首先標記出全部須要回收的對象,而後回收全部須要回收的對象,整個過程其實前一節講對象標記斷定的時候已經基本介紹完了。說它是最基礎的收集算法緣由是後續的收集算法都是基於這種思路並優化其缺點獲得的。它的主要缺點有兩個,一是效率問題,標記和清理兩個過程效率都不高,二是空間問題,標記清理以後會產生大量不連續的內存碎片,空間碎片太多可能會致使後續使用中沒法找到足夠的連續內存而提早觸發另外一次的垃圾蒐集動做。
爲了解決效率問題,一種稱爲「複製」(Copying)的蒐集算法出現,它將可用內存劃分爲兩塊,每次只使用其中的一塊,當半區內存用完了,僅將還存活的對象複製到另一塊上面,而後就把原來整塊內存空間一次過清理掉。這樣使得每次內存回收都是對整個半區的回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存就能夠了,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,未免過高了一點。
如今的商業虛擬機中都是用了這一種收集算法來回收新生代,IBM有專門研究代表新生代中的對象98%是朝生夕死的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊survivor,當回收時將eden和survivor還存活的對象一次過拷貝到另一塊survivor空間上,而後清理掉eden和用過的survivor。Sun Hotspot虛擬機默認eden和survivor的大小比例是8:1,也就是每次只有10%的內存是「浪費」的。固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有10%之內的對象存活,當survivor空間不夠用時,須要依賴其餘內存(譬如老年代)進行分配擔保(Handle Promotion)。
複製收集算法在對象存活率高的時候,效率有所降低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保用於應付半區內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。所以人們提出另一種「標記-整理」(Mark-Compact)算法,標記過程仍然同樣,但後續步驟不是進行直接清理,而是令全部存活的對象一端移動,而後直接清理掉這端邊界之外的內存。
當前商業虛擬機的垃圾收集都是採用「分代收集」(Generational Collecting)算法,這種算法並無什麼新的思想出現,只是根據對象不一樣的存活週期將內存劃分爲幾塊。通常是把Java堆分做新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法,譬如新生代每次GC都有大批對象死去,只有少許存活,那就選用複製算法只須要付出少許存活對象的複製成本就能夠完成收集。
垃圾收集器
垃圾收集器就是收集算法的具體實現,不一樣的虛擬機會提供不一樣的垃圾收集器。而且提供參數供用戶根據本身的應用特色和要求組合各個年代所使用的收集器。本文討論的收集器基於Sun Hotspot虛擬機1.6版。
圖1.Sun JVM1.6的垃圾收集器
圖1展現了1.6中提供的6種做用於不一樣年代的收集器,兩個收集器之間存在連線的話就說明它們能夠搭配使用。在介紹着些收集器以前,咱們先明確一個觀點:沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。
1.Serial收集器
單線程收集器,收集時會暫停全部工做線程(咱們將這件事情稱之爲Stop The World,下稱STW),使用複製收集算法,虛擬機運行在Client模式時的默認新生代收集器。
2.ParNew收集器
ParNew收集器就是Serial的多線程版本,除了使用多條收集線程外,其他行爲包括算法、STW、對象分配規則、回收策略等都與Serial收集器一摸同樣。對應的這種收集器是虛擬機運行在Server模式的默認新生代收集器,在單CPU的環境中,ParNew收集器並不會比Serial收集器有更好的效果。
3.Parallel Scavenge收集器
Parallel Scavenge收集器(下稱PS收集器)也是一個多線程收集器,也是使用複製算法,但它的對象分配規則與回收策略都與ParNew收集器有所不一樣,它是以吞吐量最大化(即GC時間佔總運行時間最小)爲目標的收集器實現,它容許較長時間的STW換取總吞吐量最大化。
4.Serial Old收集器
Serial Old是單線程收集器,使用標記-整理算法,是老年代的收集器,上面三種都是使用在新生代收集器。
5.Parallel Old收集器
老年代版本吞吐量優先收集器,使用多線程和標記-整理算法,JVM 1.6提供,在此以前,新生代使用了PS收集器的話,老年代除Serial Old外別無選擇,由於PS沒法與CMS收集器配合工做。
6.CMS(Concurrent Mark Sweep)收集器
CMS是一種以最短停頓時間爲目標的收集器,使用CMS並不能達到GC效率最高(整體GC時間最小),但它能儘量下降GC時服務的停頓時間,這一點對於實時或者高交互性應用(譬如證券交易)來講相當重要,這類應用對於長時間STW通常是不可容忍的。CMS收集器使用的是標記-清除算法,也就是說它在運行期間會產生空間碎片,因此虛擬機提供了參數開啓CMS收集結束後再進行一次內存壓縮。
內存分配與回收策略
瞭解GC其中很重要一點就是了解JVM的內存分配策略:即對象在哪裏分配和對象何時回收。
關於對象在哪裏分配,往大方向講,主要就在堆上分配,但也可能通過JIT進行逃逸分析後進行標量替換拆散爲原子類型在棧上分配,也可能分配在DirectMemory中(詳見本文第一章)。往細節處講,對象主要分配在新生代eden上,也可能會直接老年代中,分配的細節決定於當前使用的垃圾收集器類型與VM相關參數設置。咱們能夠經過下面代碼來驗證一下Serial收集器(ParNew收集器的規則與之徹底一致)的內存分配和回收的策略。讀者看完Serial收集器的分析後,不妨本身根據JVM參數文檔寫一些程序去實踐一下其它幾種收集器的分配策略。
清單1:內存分配測試代碼
算法
規則一:一般狀況下,對象在eden中分配。當eden沒法分配時,觸發一次Minor GC。
執行testAllocation()方法後輸出了GC日誌以及內存分配情況。-Xms20M -Xmx20M -Xmn10M這3個參數肯定了Java堆大小爲20M,不可擴展,其中10M分配給新生代,剩下的10M即爲老年代。-XX:SurvivorRatio=8決定了新生代中eden與survivor的空間比例是1:8,從輸出的結果也清晰的看到「eden space 8192K、from space 1024K、to space 1024K」的信息,新生代總可用空間爲9216K(eden+1個survivor)。
咱們也注意到在執行testAllocation()時出現了一次Minor GC,GC的結果是新生代6651K變爲148K,而總佔用內存則幾乎沒有減小(由於幾乎沒有可回收的對象)。此次GC是發生的緣由是爲allocation4分配內存的時候,eden已經被佔用了6M,剩餘空間已不足分配allocation4所需的4M內存,所以發生Minor GC。GC期間虛擬機發現已有的3個2M大小的對象所有沒法放入survivor空間(survivor空間只有1M大小),因此直接轉移到老年代去。GC後4M的allocation4對象分配在eden中。
清單2:testAllocation()方法輸出結果
[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000)
to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)
compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.
規則二:配置了PretenureSizeThreshold的狀況下,對象大於設置值將直接在老年代分配。
執行testPretenureSizeThreshold()方法後,咱們看到eden空間幾乎沒有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation對象直接就分配在老年代中,則是由於PretenureSizeThreshold被設置爲3M,所以超過3M的對象都會直接從老年代分配。
清單3:
Heap
def new generation total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 8% used [0x029d0000, 0x02a77e98, 0x031d0000)
from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
compacting perm gen total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)
the space 12288K, 17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)
No shared spaces configured.
規則三:在eden通過GC後存活,而且survivor能容納的對象,將移動到survivor空間內,若是對象在survivor中繼續熬過若干次回收(默認爲15次)將會被移動到老年代中。回收次數由MaxTenuringThreshold設置。
分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設置來執行testTenuringThreshold(),方法中allocation1對象須要256K內存,survivor空間能夠容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後很是乾淨的變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代survivor空間,這時候新生代仍然有404KB被佔用。
清單4:
MaxTenuringThreshold=1
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 414664 bytes, 414664 total
: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)
the space 10240K, 43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)
compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.
MaxTenuringThreshold=15
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 414664 bytes, 414664 total
: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 2: 414520 bytes, 414520 total
: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
from space 1024K, 39% used [0x031d0000, 0x03235338, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.
規則四:若是在survivor空間中相同年齡全部對象大小的累計值大於survivor空間的一半,大於或等於個年齡的對象就能夠直接進入老年代,無需達到MaxTenuringThreshold中要求的年齡。
執行testTenuringThreshold2()方法,並將設置-XX:MaxTenuringThreshold=15,發現運行結果中survivor佔用仍然爲0%,而老年代比預期增長了6%,也就是說allocation一、allocation2對象都直接進入了老年代,而沒有等待到15歲的臨界年齡。由於這2個對象加起來已經到達了512K,而且它們是同年的,知足同年對象達到survivor空間的一半規則。咱們只要註釋掉其中一個對象new操做,就會發現另一個就不會晉升到老年代中去了。
清單5:
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 676824 bytes, 676824 total
: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)
the space 10240K, 46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)
compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
the space 12288K, 17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)
No shared spaces configured.
規則五:在Minor GC觸發時,會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間,若是大於,改成直接進行一次Full GC,若是小於則查看HandlePromotionFailure設置看看是否容許擔保失敗,若是容許,那仍然進行Minor GC,若是不容許,則也要改成進行一次Full GC。
前面提到過,新生代纔有複製收集算法,但爲了內存利用率,只使用其中一個survivor空間來做爲輪換備份,所以當出現大量對象在GC後仍然存活的狀況(最極端就是GC後全部對象都存活),就須要老年代進行分配擔保,把survivor沒法容納的對象直接放入老年代。與生活中貸款擔保相似,老年代要進行這樣的擔保,前提就是老年代自己還有容納這些對象的剩餘空間,一共有多少對象在GC以前是沒法明確知道的,因此取以前每一次GC晉升到老年代對象容量的平均值與老年代的剩餘空間進行比較決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態機率的手段,也就是說若是某次Minor GC存活後的對象突增,大大高於平均值的話,依然會致使擔保失敗,這樣就只好在失敗後從新進行一次Full GC。雖然擔保失敗時作的繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure打開,避免Full GC過於頻繁。
清單6:
HandlePromotionFailure = false
[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
[GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
總結
本章介紹了垃圾收集的算法、6款主要的垃圾收集器,以及經過代碼實例具體介紹了新生代串行收集器對內存分配及回收的影響。
GC在不少時候都是系統併發度的決定性因素,虛擬機之因此提供多種不一樣的收集器,提供大量的調節參數,是由於只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最好的性能。沒有固定收集器、參數組合,也沒有最優的調優方法,虛擬機也沒有什麼必然的行爲。筆者看過一些文章,撇開具體場景去談論老年代達到92%會觸發Full GC(92%應當來自CMS收集器觸發的默認臨界點)、98%時間在進行垃圾收集系統會拋出OOM異常(98%應該來自parallel收集器收集時間比率的默認臨界點)其實意義並不太大。所以學習GC若是要到實踐調優階段,必須瞭解每一個具體收集器的行爲、優點劣勢、調節參數。數組
2. 一次Java垃圾收集調優實戰緩存
http://www.iteye.com/topic/212967多線程
GC調優是個很實驗很伽利略的活兒,GC日誌是先決的數據參考和最終驗證:併發
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps(GC發生的時間)
-XX:+PrintGCApplicationStoppedTime(GC消耗了多少時間)
-XX:+PrintGCApplicationConcurrentTime(GC之間運行了多少時間)
配置參數:-XX:+UseConcMarkSweepGC
已默認無需配置的參數:-XX:+UseParNewGC(Parallel收集新生代) -XX:+CMSPermGenSweepingEnabled(CMS收集持久代) -XX:UseCMSCompactAtFullCollection(full gc時壓縮年老代)框架
初始效果:1g堆內存的新生代約60m,minor gc約5-20毫秒,full gc約130毫秒。less
配置參數: -XX:+UseParallelGC -XX:+UseParallelOldGC(Parallel收集年老代,從JDK6.0開始支持)jvm
已默認無需配置的參數: -XX:+UseAdaptiveSizePolicy(動態調整新生代大小)
初始效果:1g堆內存的新生代約90-110m(動態調整),minor gc約5-20毫秒,full gc有無UseParallelOldGC 參數分別爲1.3/1.1秒,差異不大。
另外-XX:MaxGCPauseMillis=100 設置minor gc的指望最大時間,JVM會以此來調整新生代的大小,但在此測試環境中對象死的太快,此參數做用不大。
Parallel收集高達1秒的暫停時間基本不可忍受,因此選擇CMS收集器。
在被壓測的Mule 2.0應用裏,每秒都有大約400M的海量短命對象產生:
對這兩個參數的調優,既要改善上面兩種狀況,又要避免新生代過大,複製次數過多形成minor gc的暫停時間過長。
優化後,大約1.1秒才發生一次minor gc,且速度依然保持在15-20ms之間。同時年老代的增加速度大大減緩,好久才發生一次full gc,
參數定稿:
-server -Xms1024m -Xmx1024m -Xmn500m -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=5
-XX:+ExplicitGCInvokesConcurrent
最後服務處理速度從1180 tps 上升到1380 tps,調整兩個參數提高17%的性能仍是筆很划算的買賣。
另外,JDK6 Update 7自帶了一個VisualVM工具,內裏就是以前也有用過的Netbean Profiler,相似JConsole同樣使用,能夠看到線程狀態,內存中對象以及方法的CPU時間等調優重要參考依據。免費捆綁啊,Sun 這樣搞法,其餘作Profiler的公司要關門了。
3.JVM gc參數設置與分析
來自:http://hi.baidu.com/i1see1you/blog/item/7ba0d250c30131481038c20c.html
一.概述
java的最大好處是自動垃圾回收,這樣就無需咱們手動的釋放對象空間了,可是也產生了相應的負效果,gc是須要時間和資源的,很差的gc會嚴重影響系統的系能,所以良好的gc是JVM的高性能的保證。JVM堆分爲新生代,舊生代和年老代,新生代可用的gc方式有:串行gc(Serial Copying),並行回收gc(Parellel Scavenge),並行gc(ParNew),舊生代和年老代可用的gc方式有串行gc(Serial MSC),並行gc(Parallel MSC),併發gc(CMS)。
二.回收方式的選擇
jvm有client和server兩種模式,這兩種模式的gc默認方式是不一樣的:
clien模式下,新生代選擇的是串行gc,舊生代選擇的是串行gc
server模式下,新生代選擇的是並行回收gc,舊生代選擇的是並行gc
通常來講咱們系統應用選擇有兩種方式:吞吐量優先和暫停時間優先,對於吞吐量優先的採用server默認的並行gc方式,對於暫停時間優先的選用併發gc(CMS)方式。
三.CMS gc
CMS,全稱Concurrent Low Pause Collector,是jdk1.4後期版本開始引入的新gc算法,在jdk5和jdk6中獲得了進一步改進,它的主要適合場景是對響應時間的重要性需求大於對吞吐量的要求,可以承受垃圾回收線程和應用線程共享處理器資源,而且應用中存在比較多的長生命週期的對象的應用。CMS是用於對tenured generation的回收,也就是年老代的回收,目標是儘可能減小應用的暫停時間,減小full gc發生的概率,利用和應用程序線程併發的垃圾回收線程來標記清除年老代。在咱們的應用中,由於有緩存的存在,而且對於響應時間也有比較高的要求,所以但願能嘗試使用CMS來替代默認的server型JVM使用的並行收集器,以便得到更短的垃圾回收的暫停時間,提升程序的響應性。
CMS並不是沒有暫停,而是用兩次短暫停來替代串行標記整理算法的長暫停,它的收集週期是這樣:
初始標記(CMS-initial-mark) -> 併發標記(CMS-concurrent-mark) -> 從新標記(CMS-remark) -> 併發清除(CMS-concurrent-sweep) ->併發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)。
其中的1,3兩個步驟須要暫停全部的應用程序線程的。第一次暫停從root對象開始標記存活的對象,這個階段稱爲初始標記;第二次暫停是在併發標記以後,暫停全部應用程序線程,從新標記併發標記階段遺漏的對象(在併發標記階段結束後對象狀態的更新致使)。第一次暫停會比較短,第二次暫停一般會比較長,而且 remark這個階段能夠並行標記。
而併發標記、併發清除、併發重設階段的所謂併發,是指一個或者多個垃圾回收線程和應用程序線程併發地運行,垃圾回收線程不會暫停應用程序的執行,若是你有多於一個處理器,那麼併發收集線程將與應用線程在不一樣的處理器上運行,顯然,這樣的開銷就是會下降應用的吞吐量。Remark階段的並行,是指暫停了全部應用程序後,啓動必定數目的垃圾回收進程進行並行標記,此時的應用線程是暫停的。
四.full gc
full gc是對新生代,舊生代,以及持久代的統一回收,因爲是對整個空間的回收,所以比較慢,系統中應當儘可能減小full gc的次數。
以下幾種狀況下會發生full gc:
《舊生代空間不足
《持久代空間不足
《CMS GC時出現了promotion failed和concurrent mode failure
《統計獲得新生代minor gc時晉升到舊生代的平均大小小於舊生代剩餘空間
《直接調用System.gc,能夠DisableExplicitGC來禁止
《存在rmi調用時,默認會每分鐘執行一次System.gc,能夠經過-Dsun.rmi.dgc.server.gcInterval=3600000來設置大點的間隔。
五.示例
下面對以下的參數進行分析:
JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4
-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log -Djava.awt.headless=true -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"
-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m
Xms,即爲jvm啓動時得JVM初始堆大小,Xmx爲jvm的最大堆大小,xmn爲新生代的大小,permsize爲永久代的初始大小,MaxPermSize爲永久代的最大空間。
-XX:SurvivorRatio=4
SurvivorRatio爲新生代空間中的Eden區和救助空間Survivor區的大小比值,默認是32,也就是說Eden區是 Survivor區的32倍大小,要注意Survivo是有兩個區的,所以Surivivor其實佔整個young genertation的1/34。調小這個參數將增大survivor區,讓對象儘可能在survitor區呆長一點,減小進入年老代的對象。去掉救助空間的想法是讓大部分不能立刻回收的數據儘快進入年老代,加快年老代的回收頻率,減小年老代暴漲的可能性,這個是經過將-XX:SurvivorRatio 設置成比較大的值(好比65536)來作到。
-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log
將虛擬機每次垃圾回收的信息寫到日誌文件中,文件名由file指定,文件格式是平文件,內容和-verbose:gc輸出內容相同。
-Djava.awt.headless=true
Headless模式是系統的一種配置模式。在該模式下,系統缺乏了顯示設備、鍵盤或鼠標。
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails
設置gc日誌的格式
-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
指定rmi調用時gc的時間間隔
-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15
採用併發gc方式,通過15次minor gc 後進入年老代六.一些常見問題1.爲了不Perm區滿引發的full gc,建議開啓CMS回收Perm區選項:
+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
2.默認CMS是在tenured generation沾滿68%的時候開始進行CMS收集,若是你的年老代增加不是那麼快,而且但願下降CMS次數的話,能夠適當調高此值:
-XX:CMSInitiatingOccupancyFraction=80