天天看點

JVM記憶體機制與常見問題排查

Java記憶體模型:Java Memory Model,簡稱JMM。整體上,JVM記憶體包含堆記憶體和線程棧記憶體,原始資料類型和對象引用在棧記憶體上,對象(及其成員變量)、靜态變量都在堆記憶體上。堆記憶體上的所有對象,可以被所有線程拿到,屬于共享區域。大部分時候,我們處理的都是堆記憶體上的問題。

JVM記憶體機制與常見問題排查

JVM的GC(垃圾回收)采用分代機制,即堆記憶體分為年輕代、老年代,年輕代裡面繼續分三塊:新生代(Eden-Space)、兩個存活區(S0、S1)。GC的大緻過程是:建立對象時會在Eden區配置設定記憶體,GC的時Eden區和S區(S0和S1中非空的那個)中存活的對象複制到另外一個空的S區。S0和S1在任何時候都有一個空區域專門存儲被年輕代GC後仍然存活的對象,這批對象在S0和S1中來回複制多次,最終達到一定存活時間的對象,被移到老年代。

在預設情況下,Eden:S0:S1的記憶體配置設定比例是:8:1:1。之是以來回複制,主要是為了解決GC後的記憶體碎片問題。

JVM采用的GC算法是:标記清除算法(Mark and Sweep)。

  1. 标記:會周遊所有可達對象,這個階段會讓所有應用程式的線程暫停,導緻STW停頓問題(Stop The

    World);

  2. 清除:不可達對象占用的記憶體被回收,以便重用;

JVM調優,很大程度上是GC的調優,下面我們先看看GC相關的知識點。為了友善後面做示範,我們建立一個Java工程(過程略)。

我們可以先來一段耗記憶體的代碼:

List<Order> orderList=new ArrayList<>();
   while(true){
   Order order=new Order();
   order.setId(1);
   order.setName("Java In Action");
   order.setPrice(20.5);
   orderList.add(order);
   System.out.println( "Hello World!"+order);
  }           

工程建議加上以下依賴,示範的時候會達成jar包,并用指令行執行:

<plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>com.learn.performance.MainApp</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>           

打包之後,在Linux上啟動運作,筆者這裡采用CentOS。下面我們看看怎麼檢視GC資訊。

首先使用jps檢視目前程序ID:

JVM記憶體機制與常見問題排查

然後使用jstat檢視GC情況:

jstat -gcutil 程序ID 間隔時間           
JVM記憶體機制與常見問題排查

解釋:

S0: 新生代中Survivor space 0區已使用空間的百分比

S1: 新生代中Survivor space 1區已使用空間的百分比

E: 新生代已使用空間的百分比

O: 老年代已使用空間的百分比

M: 永久帶已使用空間的百分比

YGC: 從應用程式啟動到目前,發生Yang GC 的次數

YGCT: 從應用程式啟動到目前,Yang GC所用的時間

FGC: 從應用程式啟動到目前,發生Full GC的次數

FGCT: 從應用程式啟動到目前,Full GC所用的時間

GCT: 從應用程式啟動到目前,用于垃圾回收的總時間

當E和O區占比一直偏大,GC頻繁時,就要開始注意系統的記憶體問題了。前面我們也提到過,GC時,會導緻系統卡頓(STW),此時就必須要考慮調優了。

使用jstat檢視GC,很多時候是為了線上快速定位問題。而在實際場景中,我們通常會考慮在整個運作階段都将GC資訊打到日志裡面,等到出問題後,可以讓運維拉日志排查問題。

要實作這個效果,需要新增啟動指令-XX:+PrintGCDetails,它可以幫助列印GC細節,然後通過-Xloggc指定GC日志的輸出檔案。其中%p是程序ID的占位符:

java -XX:+PrintGCDetails -Xloggc:log/gc_%p.log  -jar PerformanceDemo-1.0.jar           

這樣,最終會在log目錄下生成gc_[程序ID].log檔案。在GC檔案裡面,大家可以看到一些預設的配置,比如初始化堆記憶體、最大堆記憶體、GC算法(UseParallelGC)假如想知道GC發生的時間,還可以加上-XX:+PrintGCDateStamps參數。

在實際場景中,我們會在啟動時指定記憶體參數,比如:

java -Xmn80m -Xms200m -Xmx200m  -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -Xloggc:log/gc_%p.log  -jar PerformanceDemo-1.0.jar           

解釋:

  • -Xmn 設定年輕代大小
  • -Xms 設定初始記憶體,此值可以和-Xmx一樣
  • -Xmx 設定JVM最大可用記憶體

啟動後,我們可以使用jmap指令檢視記憶體使用情況:

jmap -heap 程序id           
JVM記憶體機制與常見問題排查

一旦發現堆記憶體占用比過大時,我們就需要搞清楚到底是哪些對象導緻的,此時可以繼續使用jmap指令來檢視存活對象數量和大小:

jmap -histo:live 程序ID > jmapinfo           

此時會把存活對象資訊打到jmapinfo這個檔案裡面,我們可以打開檔案看下:

JVM記憶體機制與常見問題排查

可以很明顯看出,我們自定義的Order對象是最多的(Double作為Order的屬性之一)。這實際上就是調優的依據之一。

當系統真正出現OOM的時候,可能就直接挂了,這個時候我們是沒辦法使用jmap指令的,那怎麼辦呢?在生産環境中,我們通常會做OOM後生産記憶體dump檔案的配置,新增的指令參數如下:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=log/heapdump.hprof           

此時一旦OOM,會自動将當時的記憶體資訊打到heapdump.hprof檔案中,我們可以下載下傳到本地,使用JDK自帶的jvisualvm工具打開檢視,效果如下:

JVM記憶體機制與常見問題排查