JVM內存機制與常見問題排查

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

JVM堆內存.png

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:
jps.png

然後使用jstat查看GC情況:

jstat -gcutil 進程ID 間隔時間

gc.jpg

解釋:

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

jmap.png

一旦發現堆內存佔用比過大時,我們就需要搞清楚到底是哪些對象導致的,此時可以繼續使用jmap命令來查看存活對象數量和大小:

jmap -histo:live 進程ID > jmapinfo

此時會把存活對象信息打到jmapinfo這個文件裡面,我們可以打開文件看下:

heaplive.jpg

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

當系統真正出現OOM的時候,可能就直接掛了,這個時候我們是沒辦法使用jmap命令的,那怎麼辦呢?在生產環境中,我們通常會做OOM後生產內存dump文件的配置,新增的命令參數如下:

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

此時一旦OOM,會自動將當時的內存信息打到heapdump.hprof文件中,我們可以下載到本地,使用JDK自帶的jvisualvm工具打開查看,效果如下:

jdump.jpg

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top