jvm調優到底需要做什麼

語言: CN / TW / HK

記憶體洩漏及解決方法

1.系統崩潰前的一些現象:

  • 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、5s
  • FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
  • 年老代的記憶體越來越大並且每次FullGC後年老代沒有記憶體被釋放

之後系統會無法響應新的請求,逐漸到達OutOfMemoryError的臨界值。

2.生成堆的dump檔案 通過JMX的MBean生成當前的Heap資訊,大小為一個3G(整個堆的大小)的hprof檔案,如果沒有啟動JMX可以通過Java的jmap命令來生成該檔案。

3.分析dump檔案

下面要考慮的是如何開啟這個3G的堆資訊檔案,顯然一般的Window系統沒有這麼大的記憶體,必須藉助高配置的Linux。當然我們可以藉助X-Window把Linux上的圖形匯入到Window。我們考慮用下面幾種工具開啟該檔案:

  1. Visual VM
  2. IBM HeapAnalyzer
  3. JDK 自帶的Hprof工具

使用這些工具時為了確保載入速度,建議設定最大記憶體為6G。使用後發現,這些工具都無法直觀地觀察到記憶體洩漏,Visual VM雖能觀察到物件大小,但看不到呼叫堆疊;HeapAnalyzer雖然能看到呼叫堆疊,卻無法正確開啟一個3G的檔案。因此,我們又選用了Eclipse專門的靜態記憶體分析工具:Mat。

4.分析記憶體洩漏

通過Mat我們能清楚地看到,哪些物件被懷疑為記憶體洩漏,哪些物件佔的空間最大及物件的呼叫關係。針對本案,在ThreadLocal中有很多的JbpmContext例項,經過調查是JBPM的Context沒有關閉所致。

另,通過Mat或JMX我們還可以分析執行緒狀態,可以觀察到執行緒被阻塞在哪個物件上,從而判斷系統的瓶頸。

5.迴歸問題

Q:為什麼崩潰前垃圾回收的時間越來越長?

A:根據記憶體模型和垃圾回收演算法,垃圾回收分兩部分:記憶體標記、清除(複製),標記部分只要記憶體大小固定時間是不變的,變的是複製部分,因為每次垃圾回收都有一些回收不掉的記憶體,所以增加了複製量,導致時間延長。所以,垃圾回收的時間也可以作為判斷記憶體洩漏的依據

Q:為什麼Full GC的次數越來越多?

A:因此記憶體的積累,逐漸耗盡了年老代的記憶體,導致新物件分配沒有更多的空間,從而導致頻繁的垃圾回收

Q:為什麼年老代佔用的記憶體越來越大?

A:因為年輕代的記憶體無法被回收,越來越多地被Copy到年老代

調優目的

再說GC堆的調優之前,我們先來聊一聊調優的目的,可能人云亦云,每個人的看法都不一樣,我就單純的說說我對調優的看法。

我認為調優的目的主要有兩個,而且調優還必須要有前提,前提是你的系統必須要調優了,什麼意思呢?比如你的服務執行速度慢,響應慢,吞吐量小,甚至出現OOM異常了,那麼這就需要調優了。

假如,你的服務執行狀態佳,響應快,吞吐量高,那就沒必要了,此時你再去調優,就有可能適得其反。

然後,調優的目的就是第一個是是你的服務執行狀態佳,響應速度快(響應時間能夠在接受範圍),或者說吞吐量高,要達到這種目的就是使GC時間(STW)合理,次數合理,Minor GC次數合理(幾小時一次),Full GC合理(一天一次或者幾天一次)。

第二個目的就是為了解決問題,比如出現了OOM,那麼調優的目的就是為了防止再次出現OOM異常。

import java.util.ArrayList;
import java.util.List;

class OOM {

 static class User{
  private String name;
  private int age;

  public User(String name, int age){
   this.name = name;
   this.age = age;
  }

 }

 public static void main(String[] args) throws InterruptedException {
  List<User> list = new ArrayList<>();
  for (int i = 0; i < Integer.MAX_VALUE; i++) {
       Tread.sleep(1000);
   System.err.println(Thread.currentThread().getName());
   User user = new User("zhangsan"+i,i);
   list.add(user);
  }
 }
}

案例程式碼很簡單,就是不斷的往一個集合裡裡面新增物件,首先初次我們啟動的命令為:

java   -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

就是純粹的設定了一些GC的列印日誌,我們可以看到不斷的設定 調優引數為:

java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k  -XX:+UseConcMarkSweepGC  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.dump OOM

觀察一段時間後,結果如下圖所示:

圖片.png 可以看到相同時間內,確實Minor GC減少了,但是時間增大了,因為複製演算法,基本都是存活的,複製需要耗費大量的效能和時間。所以,調優要有取捨,取得一個平衡點,效能、狀態達到佳就OK了,並沒最佳的狀態,這就是調優的基本法則,而且調優也是一個細活,所謂慢工出細活,需要耗費大量的時間,慢慢調,不斷的做對比。