1. 程式人生 > 其它 >記錄一次OOM的排查過程以及記憶體分析、解決方案

記錄一次OOM的排查過程以及記憶體分析、解決方案

  在測試環境中開啟的堆大小是4g。但是卻發生了OOM。

  發生OOM的場景是: 上傳Excel 之後進行資料的清洗,然後清洗完成之後會將清洗掉的、清洗後的資料再次備份到磁碟中;同時將清洗後的資料入關係型資料庫。(解析Excel 用的是POI, 資料清洗用的是Tablesaw, 且清洗的操作都是在記憶體中處理的)

  記錄下此次OOM的排查過程。

1. 前置知識

  關於JVM除錯的前置知識。

0. 丟擲OOM的前提

The parallel collector throws an OutOfMemoryError if too much time is being spent in garbage collection (GC): If more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, then an OutOfMemoryError is thrown. This feature is designed to prevent applications from running for
an extended period of time while making little or no progress because the heap is too small. If necessary, this feature can be disabled by adding the option -XX:-UseGCOverheadLimit to the command line.

1. 記憶體區域

JVM的記憶體區域分為五塊,隨執行緒消亡的包括 本地方法棧、虛擬機器棧(棧)、PC(程式計數器),執行緒共享的區域包括:堆、方法區(JDK7的永久代,JDK8的MetaSpace元空間)。

-Xms2g 可以指定初始化堆的大小,-Xmx2g可以指定最大堆的大小。 其中堆分為新生代(Eden區、From Survivor區和To Survivor)、老年代。新生代和老年代的比例預設是1:2,也就是新生代佔堆的1/3,老年代佔堆的2/3(–XX:NewRatio可以調節新生代和老年代比例)。新生代Eden和兩個Survivor的比例是8:1:1。(–XX:SurvivorRatio可以調節E區和兩個S區比例)。

關於未指定初始化堆和最大堆的情況下,JVM會根據機器的記憶體進行計算。參考 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#default_heap_size

不指定-Xms 初始化堆大小的情況下 初始化是機器記憶體的1/64 , 最小是8m。不指定-Xmx 最大堆的大小是1/4 機器記憶體。

檢視預設的初始堆和最大堆的大小:我的機器是16G執行記憶體

C:\Users\xxx>java -XX:+PrintFlagsFinal -version  | findstr HeapSize
    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 264241152                           {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 4206886912                          {product}

換成M之後InitialHeapSize 是 252 m, MaxHeapSize 是 4012 M

也可以用java 程式檢視總堆以及剩餘的堆記憶體:

        long totalMemory1 = Runtime.getRuntime().totalMemory();
        long freeMemory1 = Runtime.getRuntime().freeMemory();

  另外JVM 有兩種模式,client模式和server 模式。-Server模式啟動時,速度較慢,但是一旦執行起來後,效能將會有很大的提升。原因是:當虛擬機器執行在-client模式的時候,使用的是一個代號為C1的輕量級編譯器, 而-server模式啟動的虛擬機器採用相對重量級,代號為C2的編譯器。 C2比C1編譯器編譯的相對徹底,服務起來之後,效能更高。

  Server 模式下: 在32位JVM下,如果實體記憶體在4G或更高,最大堆大小可以提升至1GB,如果是在64位JVM下,如果實體記憶體在128GB或更高,最大堆大小可以提升至32GB。

C:\Users\xxx>java -version
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)

Server VM 指定預設是Server 模式。

2. 關於排查工具

(1) jps檢視當前的Java 程序以及引數和啟動的主類:

C:\Users\xxx\Desktop\OOMTest>jps -l -v | findstr Plain
165288 com.xm.ggn.test.PlainTest2 -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:65507,suspend=y,server=n -Xms2g -Xmx2g -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IdeaIC2020.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8

(2)jmap---Memory Map for Java,生成虛擬機器的記憶體轉儲快照(heapdump檔案)

C:\Users\xxx\Desktop\OOMTest>jmap -heap 165288    #檢視堆記憶體
Attaching to process ID 165288, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.291-b10

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2147483648 (2048.0MB)
   NewSize                  = 715653120 (682.5MB)
   MaxNewSize               = 715653120 (682.5MB)
   OldSize                  = 1431830528 (1365.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 537395200 (512.5MB)
   used     = 115448968 (110.10071563720703MB)
   free     = 421946232 (402.39928436279297MB)
   21.483066465796494% used
From Space:
   capacity = 89128960 (85.0MB)
   used     = 89112856 (84.9846420288086MB)
   free     = 16104 (0.01535797119140625MB)
   99.98193179859834% used
To Space:
   capacity = 89128960 (85.0MB)
   used     = 0 (0.0MB)
   free     = 89128960 (85.0MB)
   0.0% used
PS Old Generation
   capacity = 1431830528 (1365.5MB)
   used     = 860691232 (820.8191223144531MB)
   free     = 571139296 (544.6808776855469MB)
   60.11125026103648% used

1809 interned Strings occupying 161856 bytes.

C:\Users\xxx\Desktop\OOMTest>jmap -dump:live,format=b,file=165288.hprof 165288    #匯出堆記憶體
Dumping heap to C:\Users\xxx\Desktop\OOMTest\165288.hprof ...
Heap dump file created

(3) jstat(JVM Statistics Monitoring Machine)是用於監視虛擬機器各種執行狀態資訊的工具。它可以顯示本地或者遠端虛擬機器程序中的類裝載、記憶體、垃圾收集、JID編譯等執行時資料,在沒有GUI的伺服器上,對於定位虛擬機器效能問題非常重要。

C:\Users\xxx\Desktop\OOMTest>jstat -gc 165288 1000 5    #1000 是間隔1s, 5是五次(不指定會一直檢測)
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
87040.0 87040.0  0.0    0.0   524800.0  5248.0  1398272.0  1020413.9  4864.0 3258.4 512.0  336.3       3    0.743   1      3.812    4.555
87040.0 87040.0  0.0    0.0   524800.0  5248.0  1398272.0  1020413.9  4864.0 3258.4 512.0  336.3       3    0.743   1      3.812    4.555
87040.0 87040.0  0.0    0.0   524800.0  5248.0  1398272.0  1020413.9  4864.0 3258.4 512.0  336.3       3    0.743   1      3.812    4.555
87040.0 87040.0  0.0    0.0   524800.0  5248.0  1398272.0  1020413.9  4864.0 3258.4 512.0  336.3       3    0.743   1      3.812    4.555
87040.0 87040.0  0.0    0.0   524800.0  5248.0  1398272.0  1020413.9  4864.0 3258.4 512.0  336.3       3    0.743   1      3.812    4.555

(4) jvisualvm.exe --- 多合一故障處理工具(重要)

可以檢視記憶體、JVM屬性、實時檢視堆記憶體資訊以及dump出堆轉儲快照以及檢視執行緒、也可以分析dump出的堆轉儲快照檔案。

這裡主要分析用它分析dump 出的堆轉儲快照檔案,其他都是GUI圖形化操作。

1》開啟jvisualvm
2》檔案-》裝入 選擇hprof檔案即可分析

例如檢視上面dump 出的檔案,匯入之後介面如下:

檢視類以及例項數量,判斷佔用記憶體多的物件:

2. 分析問題

  場景是:POI 讀取60M 50W 行Excel資料記憶體溢位。 這裡解釋下。 60M的excel 資料載入到JVM中轉為List<Map> 結構就會佔用1g的記憶體。

  場景復現: 下面是自己main 方法啟動復現場景。

1. 關於啟動引數

設定記憶體溢位後生成dump 檔案, 堆初始化和最大都是4g:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./  -Xms4g -Xmx4g

也可以增加列印GC詳細資訊的引數

-XX:+PrintGCDetails

2. 檢視日誌

  貼出來一段日誌如下:

java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to ./\java_pid144064.hprof ...
Heap dump file created [4753175326 bytes in 15.657 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.util.Arrays.copyOfRange(Arrays.java:3664)
    at java.lang.String.<init>(String.java:207)
    at com.sun.org.apache.xerces.internal.xni.XMLString.toString(XMLString.java:189)
    at com.sun.org.apache.xerces.internal.impl.XMLScanner.scanCharReferenceValue(XMLScanner.java:1337)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:3055)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:605)
    at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:113)

3. 問題排查

(1) 拿到java_pid144064.hprof

(2) 用jvisualvm 裝入後檢視

(2) 檢視類例項: 可以看到佔用記憶體最大的類是char[] 和 String

  poi 在讀取過程中會一次性載入檔案,然後轉為字串,最後會轉char[] 陣列物件,最終導致OOM。

4. 解決

最後採用阿里的easyexcel 進行讀取避免了這個問題。

後記: 後來匯出的時候也有類似的問題,最終是匯出採用csv 的方式進行匯出,這樣可以解決大量的記憶體。