天天看點

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

一、Java垃圾回收概況

Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要差別之一,作為Java開發者,一般不需要專門編寫記憶體回收和垃圾清理代碼,對記憶體洩露和溢出的問題,也不需要像C程式員那樣戰戰兢兢。這是因為在Java虛拟機中,存在自動記憶體管理和垃圾清掃機制。概括地說,該機制對JVM(Java Virtual Machine)中的記憶體進行标記,并确定哪些記憶體需要回收,根據一定的回收政策,自動的回收記憶體,永不停息(Nerver Stop)的保證JVM中的記憶體空間,防止出現記憶體洩露和溢出問題。 

Java GC機制主要完成3件事:确定哪些記憶體需要回收,确定什麼時候需要執行GC,如何執行GC。學習Java GC機制,可以幫助我們在日常工作中排查各種記憶體溢出或洩露問題,解決性能瓶頸,達到更高的并發量,寫出更高效的程式。

1、如何确定某個對象是“垃圾”?

如果确定某個對象是“垃圾”?

引用計數法:在Java中是通過引用來和對象進行關聯的,也就是說如果要操作對象,必須通過引用來進行。那麼很顯然一個簡單的辦法就是通過引用計數來判斷一個對象是否可以被回收。不失一般性,如果一個對象沒有任何引用與之關聯,則說明該對象基本不太可能在其他地方被使用到,那麼這個對象就成為可被回收的對象了。這種方式的特點是實作簡單,而且效率較高,但是它無法解決循環引用的問題。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

可達性分析法:該方法的基本思想是通過一系列的“GC Roots”對象作為起點進行搜尋,如果在“GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。不過要注意的是被判定為不可達的對象不一定就會成為可回收對象。被判定為不可達的對象要成為可回收對象必須至少經曆兩次标記過程,如果在這兩次标記過程中仍然沒有逃脫成為可回收對象的可能性,則基本上就真的成為可回收對象了。可作為 GC Roots 的對象:

  • 虛拟機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜态屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中 JNI(即一般說的 Native 方法) 引用的對象
Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

2、典型的垃圾收集算法 

在确定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裡面涉及到一個問題是:如何高效地進行垃圾回收。由于Java虛拟機規範并沒有對如何實作垃圾收集器做出明确的規定,是以各個廠商的虛拟機可以采用不同的方式來實作垃圾收集器,是以在此隻讨論幾種常見的垃圾收集算法的核心思想。 

Mark-Sweep(标記-清除)算法:這是最基礎的垃圾回收算法,之是以說它是最基礎的是因為它最容易實作,思想也是最簡單的。标記-清除算法分為兩個階段:标記階段和清除階段。标記階段的任務是标記出所有需要被回收的對象,清除階段就是回收被标記的對象所占用的空間。比較嚴重的問題就是容易産生記憶體碎片,碎片太多可能會導緻後續過程中需要為大對象配置設定空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。具體過程如下圖所示:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Copying(複制)算法:為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體碎片的問題。這種算法雖然實作簡單,運作高效且不容易産生記憶體碎片,但是卻對記憶體空間的使用做出了高昂的代價,因為能夠使用的記憶體縮減到原來的一半。很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麼Copying算法的效率将會大大降低。具體過程如下圖所示:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Mark-Compact(标記-整理)算法:為了解決Copying算法的缺陷,充分利用記憶體空間,提出了Mark-Compact算法。該算法标記階段和Mark-Sweep一樣,但是在完成标記之後,它不是直接清理可回收對象,而是将存活對象都向一端移動,然後清理掉端邊界以外的記憶體。具體過程如下圖所示:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Generational Collection(分代收集)算法:分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期将記憶體劃分為若幹個不同的區域。一般情況下将堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時隻有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點采取最适合的收集算法。目前大部分垃圾收集器對于新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要複制的操作次數較少,但是實際中并不是按照1:1的比例來劃分新生代的空間的,一般來說是将新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,将Eden和Survivor中還存活的對象複制到另一塊Survivor空間中,然後清理掉Eden和剛才使用過的Survivor空間。而由于老年代的特點是每次回收都隻回收少量對象,一般使用的是Mark-Compact算法。

對象優先在 Eden 區配置設定:多數情況,對象都在新生代 Eden 區配置設定。當 Eden 區配置設定沒有足夠的空間進行配置設定時,虛拟機将會發起一次 Minor GC。如果本次 GC 後還是沒有足夠的空間,則将啟用配置設定擔保機制在老年代中配置設定記憶體。這裡我們提到 Minor GC,如果你仔細觀察過 GC 日常,通常我們還能從日志中發現 Major GC/Full GC。

  1. Minor GC 是指發生在新生代的 GC,因為 Java 對象大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非常快;
  2. Major GC/Full GC 是指發生在老年代的 GC,出現了 Major GC 通常會伴随至少一次 Minor GC。Major GC 的速度通常會比 Minor GC 慢 10 倍以上。

大對象直接進入老年代:所謂大對象是指需要大量連續記憶體空間的對象,頻繁出現大對象是緻命的,會導緻在記憶體還有不少空間的情況下提前觸發 GC 以擷取足夠的連續空間來安置新對象。如果大對象直接在新生代配置設定就會導緻 Eden 區和兩個 Survivor 區之間發生大量的記憶體複制。是以對于大對象都會直接在老年代進行配置設定。

長期存活對象将進入老年代:虛拟機采用分代收集的思想來管理記憶體,那麼記憶體回收時就必須判斷哪些對象應該放在新生代,哪些對象應該放在老年代。是以虛拟機給每個對象定義了一個對象年齡的計數器,如果對象在 Eden 區出生,并且能夠被 Survivor 容納,将被移動到 Survivor 空間中,這時設定對象年齡為 1。對象在 Survivor 區中每「熬過」一次 Minor GC 年齡就加 1,當年齡達到一定程度(預設 15) 就會被晉升到老年代。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優
Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

注意:在堆區之外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部分内容:廢棄常量和無用的類。

  1. 判斷廢棄常量:一般是判斷沒有該常量的引用。
  2. 判斷無用的類:要以下三個條件都滿足
    1. 該類所有的執行個體都已經回收,也就是 Java 堆中不存在該類的任何執行個體;
    2. 加載該類的 ClassLoader 已經被回收;
    3. 該類對應的 java.lang.Class 對象沒有任何地方呗引用,無法在任何地方通過反射通路該類的方法;

Java8中已經移除了永久代,新加了一個叫做中繼資料區的native記憶體區。

二、Java垃圾收集器

如果說垃圾收集算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實作。下圖展示了7種作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用于回收整個Java堆的G1收集器。不同收集器之間的連線表示它們可以搭配使用。
Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Serial收集器 (複制算法):新生代收集器,使用停止複制算法,使用一個線程進行GC,串行,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運作進行記憶體回收(這也是虛拟機在Client模式下運作的預設值)。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

ParNew收集器(複制算法):新生代收集器,使用停止複制算法,Serial收集器的多線程版,用多個線程進行GC,并行,其它工作線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集記憶體;使用-XX:ParallelGCThreads來設定執行記憶體回收的線程數。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Parallel Scavenge 收集器(複制算法):新生代收集器,使用停止複制算法,關注CPU吞吐量,即運作使用者代碼的時間/總時間,比如:JVM運作100分鐘,其中運作使用者代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,适合運作背景運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,是以适 合使用者互動,提高使用者體驗)。使用-XX:+UseParallelGC開關控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的預設值);使用-XX:GCTimeRatio來設定使用者執行時間占總時間的比例,預設99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設定GC的最大停頓時間(這個參數隻對Parallel Scavenge有效),用開關參數-XX:+UseAdaptiveSizePolicy可以進行動态控制,如自動調整Eden/Survivor比例,老年代對象年齡,新生代大小等,這個參數在ParNew下沒有。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Serial Old收集器(标記-整理算法):老年代收集器,單線程收集器,串行,使用标記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是将廢棄的對象幹掉,隻留幸存的對象,壓縮是将移動對象,将空間填滿保證記憶體分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行标記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Parallel Old收集器(标記-整理算法):老年代收集器,多線程,并行,多線程機制與Parallel Scavenge差不錯,使用标記整理(與Serial Old不同,這裡的整理是Summary(彙總)和Compact(壓縮),彙總的意思就是将幸存的對象複制到預先準備好的區域,而不是像Sweep(清理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現後(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分展現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

CMS(Concurrent Mark Sweep)收集器(标記-清除算法):老年代收集器,緻力于擷取最短回收停頓時間(即縮短垃圾回收的時間),使用标記清除算法,多線程,優點是并發收集(使用者線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行記憶體回收,優先使用ParNew+CMS(原因見後面),當使用者線程記憶體不足時,采用備用方案Serial Old收集。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

G1(Garbage First)收集器 (标記-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基于“标記-整理”算法實作,也就是說不會産生記憶體碎片。此外,G1收集器不同于之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限于新生代或老年代。并行與并發、分代收集、空間整合、可預測停頓。

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

Serial是串行的,Parallel收集器是并行的,而CMS收集器是并發的。注意并發(Concurrent)和并行(Parallel)的差別:

  1. 并發:是指使用者線程與GC線程同時執行(不一定是并行,可能交替,但總體上是在同時執行的),不需要停頓使用者線程(其實在CMS中使用者線程還是需要停頓的,隻是非常短,GC線程在另一個CPU上執行);
  2. 并行:是指多個GC線程并行工作,但此時使用者線程是暫停的;

三、JVM監控與調優

1、Jvm參數初識

 在Java虛拟機的參數中,有3種表示方法,用“ps -ef |grep "java"指令,可以得到目前Java程序的所有啟動參數和配置參數:

  • 标準參數(-),所有的JVM實作都必須實作這些參數的功能,而且向後相容;
  • 非标準參數(-X),預設jvm實作這些參數的功能,但是并不保證所有jvm實作都滿足,且不保證向後相容;
  • 非Stable參數(-XX),此類參數各個jvm實作會有所不同,将來可能會随時取消,需要慎重使用(但是,這些參數往往是非常有用的);

額外的,-DpropertyName=“value”的形式定義了一些全局屬性值。

  • -Xmn:新生代記憶體大小的最大值,包括E區和兩個S區的總和,使用方法如:-Xmn65535,-Xmn1024k,-Xmn512m,-Xmn1g (-Xms,-Xmx也是種寫法);
  • -Xms:初始堆的大小,也是堆大小的最小值,預設值是總共的實體記憶體/64(且小于1G),預設情況下,當堆中可用記憶體小于40%(這個值可以用-XX: MinHeapFreeRatio 調整,如-X:MinHeapFreeRatio=30)時,堆記憶體會開始增加,一直增加到-Xmx的大小;
  • -Xmx:堆的最大值,預設值是總共的實體記憶體/64(且小于1G),如果Xms和Xmx都不設定,則兩者大小會相同,預設情況下,當堆中可用記憶體大于70%(這個值可以用-XX: MaxHeapFreeRatio 調整,如-X:MaxHeapFreeRatio=60)時,堆記憶體會開始減少,一直減小到-Xms的大小;整個堆的大小=年輕代大小+年老代大小,堆的大小不包含持久代大小,如果增大了年輕代,年老代相應就會減小,官方預設的配置為年老代大小/年輕代大小=2/1左右(使用-XX:NewRatio可以設定-XX:NewRatio=5,表示年老代/年輕代=5/1);建議在開發測試環境可以用Xms和Xmx分别設定最小值最大值,但是線上上生産環境,Xms和Xmx設定的值必須一樣,原因與年輕代一樣——防止抖動;
  • -Xss:這個參數用于設定每個線程的棧記憶體,預設1M,一般來說是不需要改的。除非代碼不多,可以設定的小點,另外一個相似的參數是-XX:ThreadStackSize,這兩個參數在1.6以前,都是誰設定在後面,誰就生效;1.6版本以後,-Xss設定在後面,則以-Xss為準,-XXThreadStackSize設定在後面,則主線程以-Xss為準,其它線程以-XX:ThreadStackSize為準。
  • -Xrs:減少JVM對作業系統信号(OS Signals)的使用(JDK1.3.1之後才有效),當此參數被設定之後,jvm将不接收控制台的控制handler,以防止與在背景以服務形式運作的JVM沖突(這個用的比較少,參考:http://www.blogjava.net/midstr/archive/2008/09/21/230265.html)。
  • -Xprof:跟蹤正運作的程式,并将跟蹤資料在标準輸出輸出;适合于開發環境調試。
  • -Xnoclassgc:關閉針對class的gc功能;因為其阻止記憶體回收,是以可能會導緻OutOfMemoryError錯誤,慎用;
  • -Xincgc: 開啟增量gc(預設為關閉);這有助于減少長時間GC時應用程式出現的停頓;但由于可能和應用程式并發執行,是以會降低CPU對應用的處理能力。
  • -Xloggc:file:與-verbose:gc功能類似,隻是将每次GC事件的相關情況記錄到一個檔案中,檔案的位置最好在本地,以避免網絡的潛在問題。若與verbose指令同時出現在指令行中,則以-Xloggc為準。

2、監控工具和方法

在JVM運作的過程中,為保證其穩定、高效,或在出現GC問題時分析問題原因,我們需要對GC進行監控。所謂監控,其實就是分析清楚目前GC的情況。其目的是鑒别JVM是否在高效的進行垃圾回收,以及有沒有必要進行調優。

通過監控GC,我們可以搞清楚很多問題,如:

1,minor GC和full GC的頻率;

2,執行一次GC所消耗的時間;

3,新生代的對象何時被移到老生代以及花費了多少時間;

4,每次GC中,其它線程暫停(Stop the world)的時間;

5,每次GC的效果如何,是否不理想;

………………

監控GC的工具分為2種:指令行工具和圖形工具;

常用的指令行工具有:

注:下面的指令都在JAVA_HOME/bin中,是java自帶的指令。如果您發現無法使用,請直接進入Java安裝目錄調用或者先設定Java的環境變量,一個簡單的辦法為:直接運作指令 export PATH=$JAVA_HOME/bin:$PATH;另外,一般的,在Linux下,下面的指令需要sudo權限,在windows下,部分指令的部分選項不能使用。

1、jps

jps指令用于查詢正在運作的JVM程序,常用的參數為:

    -q:隻輸出LVMID,省略主類的名稱

    -m:輸出虛拟機程序啟動時傳給主類main()函數的參數

    -l:輸出主類的全類名,如果程序執行的是Jar包,輸出Jar路徑

    -v:輸出虛拟機程序啟動時JVM參數

指令格式:jps [option] [hostid]

一個簡單的例子:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

在上圖中,有一個vid為309的apache程序在提供web服務。

2、jstat

jstat可以實時顯示本地或遠端JVM程序中類裝載、記憶體、垃圾收集、JIT編譯等資料(如果要顯示遠端JVM資訊,需要遠端主機開啟RMI支援)。如果在服務啟動時沒有指定啟動參數-verbose:gc,則可以用jstat實時檢視gc情況。

jstat有如下選項:

   -class:監視類裝載、解除安裝數量、總空間及類裝載所耗費的時間

   -gc:監聽Java堆狀況,包括Eden區、兩個Survivor區、老年代、永久代等的容量,以用空間、GC時間合計等資訊

   -gccapacity:監視内容與-gc基本相同,但輸出主要關注java堆各個區域使用到的最大和最小空間

   -gcutil:監視内容與-gc基本相同,但輸出主要關注已使用空間占總空間的百分比

   -gccause:與-gcutil功能一樣,但是會額外輸出導緻上一次GC産生的原因

   -gcnew:監視新生代GC狀況

   -gcnewcapacity:監視内同與-gcnew基本相同,輸出主要關注使用到的最大和最小空間

   -gcold:監視老年代GC情況

   -gcoldcapacity:監視内同與-gcold基本相同,輸出主要關注使用到的最大和最小空間

   -gcpermcapacity:輸出永久代使用到最大和最小空間

   -compiler:輸出JIT編譯器編譯過的方法、耗時等資訊

   -printcompilation:輸出已經被JIT編譯的方法

指令格式:jstat [option vmid [interval[s|ms] [count]]]

jstat可以監控遠端機器,指令格式中VMID和LVMID特别說明:如果是本地虛拟機程序,VMID和LVMID是一緻的,如果是遠端虛拟機程序,那麼VMID格式是: [protocol:][//]lvmid[@hostname[:port]/servername],如果省略interval和count,則隻查詢一次

檢視gc情況的例子:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

在圖中,指令sudo jstat -gc 309 1000 5代表着:搜集vid為309的java程序的整體gc狀态, 每1000ms收集一次,共收集5次;XXXC表示該區容量,XXXU表示該區使用量,各列解釋如下:

S0C:S0區容量(S1區相同,略)

S0U:S0區已使用

EC:E區容量

EU:E區已使用

OC:老年代容量

OU:老年代已使用

PC:Perm容量

PU:Perm區已使用

YGC:Young GC(Minor GC)次數

YGCT:Young GC總耗時

FGC:Full GC次數

FGCT:Full GC總耗時

GCT:GC總耗時

用gcutil檢視記憶體的例子:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

圖中的各列與用gc參數時基本一緻,不同的是這裡顯示的是已占用的百分比,如S0為86.53,代表着S0區已使用了86.53%

3、jinfo

用于查詢目前運作這的JVM屬性和參數的值。

jinfo可以使用如下選項:

   -flag:顯示未被顯示指定的參數的系統預設值

   -flag [+|-]name或-flag name=value: 修改部分參數

   -sysprops:列印虛拟機程序的System.getProperties()

 指令格式:jinfo [option] pid

4、jmap

用于顯示目前Java堆和永久代的詳細資訊(如目前使用的收集器,目前的空間使用率等)

   -dump:生成java堆轉儲快照

   -heap:顯示java堆詳細資訊(隻在Linux/Solaris下有效)

   -F:當虛拟機程序對-dump選項沒有響應時,可使用這個選項強制生成dump快照(隻在Linux/Solaris下有效)

   -finalizerinfo:顯示在F-Queue中等待Finalizer線程執行finalize方法的對象(隻在Linux/Solaris下有效)

   -histo:顯示堆中對象統計資訊

   -permstat:以ClassLoader為統計口徑顯示永久代記憶體狀态(隻在Linux/Solaris下有效)

 指令格式:jmap [option] vmid

其中前面3個參數最重要,如:

檢視對詳細資訊:sudo jmap -heap 309

生成dump檔案: sudo jmap -dump:file=./test.prof 309

部分使用者沒有權限時,采用admin使用者:sudo -u admin -H  jmap -dump:format=b,file=檔案名.hprof pid

檢視目前堆中對象統計資訊:sudo  jmap -histo 309:該指令顯示3列,分别為對象數量,對象大小,對象名稱,通過該指令可以檢視是否記憶體中有大對象;

有的使用者可能沒有jmap權限:sudo -u admin -H jmap -histo 309 | less

5、jhat

用于分析使用jmap生成的dump檔案,是JDK自帶的工具,使用方法為: jhat -J -Xmx512m [file]

不過jhat沒有mat好用,推薦使用mat(Eclipse插件: http://www.eclipse.org/mat ),mat速度更快,而且是圖形界面。

6,jstack

用于生成目前JVM的所有線程快照,線程快照是虛拟機每一條線程正在執行的方法,目的是定位線程出現長時間停頓的原因。

   -F:當正常輸出的請求不被響應時,強制輸出線程堆棧

   -l:除堆棧外,顯示關于鎖的附加資訊

   -m:如果調用到本地方法的話,可以顯示C/C++的堆棧

指令格式:jstack [option] vmid

7、-verbosegc

-verbosegc是一個比較重要的啟動參數,記錄每次gc的日志,下面的表格對比了jstat和-verbosegc:

jstat -verbosegc
監控對象 運作在本機的Java應用可以把日志輸出到終端上,或者借助jstatd指令通過網絡連接配接遠端的Java應用。 隻有那些把-verbogc作為啟動參數的JVM。
輸出資訊 堆狀态(已用空間,最大限制,GC執行次數/時間,等等) 執行GC前後新生代和老年代空間大小,GC執行時間。
輸出時間

Every designated time

每次設定好的時間。

每次GC發生的時候。
用途 觀察堆空間變化情況 了解單次GC産生的效果。

與-verbosegc配合使用的一些常用參數為:

   -XX:+PrintGCDetails,列印GC資訊,這是-verbosegc預設開啟的選項

   -XX:+PrintGCTimeStamps,列印每次GC的時間戳

   -XX:+PrintHeapAtGC:每次GC時,列印堆資訊

   -XX:+PrintGCDateStamps (from JDK 6 update 4) :列印GC日期,适合于長期運作的伺服器

   -Xloggc:/home/admin/logs/gc.log:制定列印資訊的記錄的日志位置

每條verbosegc列印出的gc日志,都類似于下面的格式:

time [GC [<collector>: <starting occupancy1> -> <ending occupancy1>(total occupancy1), <pause time1> secs] <starting occupancy3> -> <ending occupancy3>(total occupancy3), <pause time3> secs]

如:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

這些選項的意義是:

time:執行GC的時間,需要添加-XX:+PrintGCDateStamps參數才有;

collector:minor gc使用的收集器的名字。

starting occupancy1:GC執行前新生代空間大小。

ending occupancy1:GC執行後新生代空間大小。

total occupancy1:新生代總大小

pause time1:因為執行minor GC,Java應用暫停的時間。

starting occupancy3:GC執行前堆區域總大小

ending occupancy3:GC執行後堆區域總大小

total occupancy3:堆區總大小

pause time3:Java應用由于執行堆空間GC(包括full GC)而停止的時間。

8、可視化工具

監控和分析GC也有一些可視化工具,比較常見的有JConsole和VisualVM,有興趣的可以看看下面的文章,在此不再贅述:

http://blog.csdn.net/java2000_wl/article/details/8049707

3、調優方法

一切都是為了這一步,調優,在調優之前,我們需要記住下面的原則:

  1. 多數的Java應用不需要在伺服器上進行GC優化;
  2. 多數導緻GC問題的Java應用,都不是因為我們參數設定錯誤,而是代碼問題;
  3. 在應用上線之前,先考慮将機器的JVM參數設定到最優(最适合);
  4. 減少建立對象的數量;
  5. 減少使用全局變量和大對象;
  6. GC優化是到最後不得已才采用的手段;
  7. 在實際使用中,分析GC情況優化代碼比優化GC參數要多得多;

GC優化的目的有兩個(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml):

  • 将轉移到老年代的對象數量降低到最小;
  • 減少full GC的執行時間;

為了達到上面的目的,一般地,你需要做的事情有:

  • 減少使用全局變量和大對象;
  • 調整新生代的大小到最合适;
  • 設定老年代的大小為最合适;
  • 選擇合适的GC收集器;

在上面的4條方法中,用了幾個“合适”,那究竟什麼才算合适,一般的,請參考上面“收集器搭配”和“啟動記憶體配置設定”兩節中的建議。但這些建議不是萬能的,需要根據您的機器和應用情況進行發展和變化,實際操作中,可以将兩台機器分别設定成不同的GC參數,并且進行對比,選用那些确實提高了性能或減少了GC時間的參數。

真正熟練的使用GC調優,是建立在多次進行GC監控和調優的實戰經驗上的,進行監控和調優的一般步驟為:

1,監控GC的狀态

使用各種JVM工具,檢視目前日志,分析目前JVM參數設定,并且分析目前堆記憶體快照和gc日志,根據實際的各區域記憶體劃分和GC執行時間,覺得是否進行優化;

2,分析結果,判斷是否需要優化

如果各項參數設定合理,系統沒有逾時日志出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化;如果GC時間超過1-3秒,或者頻繁GC,則必須優化;

注:如果滿足下面的名額,則一般不需要進行GC:

  • Minor GC執行時間不到50ms;
  • Minor GC執行不頻繁,約10秒一次;
  • Full GC執行時間不到1s;
  • Full GC執行頻率不算頻繁,不低于10分鐘1次;

3,調整GC類型和記憶體配置設定

如果記憶體配置設定過大或過小,或者采用的GC收集器比較慢,則應該優先調整這些參數,并且先找1台或幾台機器進行beta,然後比較優化過的機器和沒有優化的機器的性能對比,并有針對性的做出最後選擇;

4,不斷的分析和調整

通過不斷的試驗和試錯,分析并找到最合适的參數

5,全面應用參數

如果找到了最合适的參數,則将這些參數應用到所有伺服器,并進行後續跟蹤。

4、調優執行個體

 上面的内容都是紙上談兵,下面我們以一些真執行個體子來進行說明:

執行個體1

 昨日發現部分開發測試機器出現異常:java.lang.OutOfMemoryError: GC overhead limit exceeded,這個異常代表:GC為了釋放很小的空間卻耗費了太多的時間,其原因一般有兩個:1,堆太小,2,有死循環或大對象。筆者首先排除了第2個原因,因為這個應用同時是線上上運作的,如果有問題,早就挂了。是以懷疑是這台機器中堆設定太小;

使用ps -ef |grep "java"檢視,發現:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

該應用的堆區設定隻有768m,而機器記憶體有2g,機器上隻跑這一個java應用,沒有其他需要占用記憶體的地方。另外,這個應用比較大,需要占用的記憶體也比較多;

筆者通過上面的情況判斷,隻需要改變堆中各區域的大小設定即可,于是改成下面的情況:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

跟蹤運作情況發現,相關異常沒有再出現。

執行個體2

一個服務系統,經常出現卡頓,分析原因,發現Full GC時間太長:

jstat -gcutil:

S0     S1    E     O       P        YGC YGCT FGC FGCT  GCT

12.16 0.00 5.18 63.78 20.32  54   2.047 5     6.946  8.993

分析上面的資料,發現Young GC執行了54次,耗時2.047秒,每次Young GC耗時37ms,在正常範圍,而Full GC執行了5次,耗時6.946秒,每次平均1.389s,資料顯示出來的問題是:Full GC耗時較長,分析該系統的是指發現,NewRatio=9,也就是說,新生代和老生代大小之比為1:9,這就是問題的原因:

1,新生代太小,導緻對象提前進入老年代,觸發老年代發生Full GC;

2,老年代較大,進行Full GC時耗時較大;

優化的方法是調整NewRatio的值,調整到4,發現Full GC沒有再發生,隻有Young GC在執行。這就是把對象控制在新生代就清理掉,沒有進入老年代(這種做法對一些應用是很有用的,但并不是對所有應用都要這麼做) 

執行個體3

一應用在性能測試過程中,發現記憶體占用率很高,Full GC頻繁,使用sudo -u admin -H  jmap -dump:format=b,file=檔案名.hprof pid 來dump記憶體,生成dump檔案,并使用Eclipse下的mat差距進行分析,發現:

Java系列之GC垃圾回收一、Java垃圾回收概況二、Java垃圾收集器三、JVM監控與調優

從圖中可以看出,這個線程存在問題,隊列LinkedBlockingQueue所引用的大量對象并未釋放,導緻整個線程占用記憶體高達378m,此時通知開發人員進行代碼優化,将相關對象釋放掉即可。

Java系列筆記(3) - Java 記憶體區域和GC機制         Java系列筆記(4) - JVM監控與調優