在測試環境中開啟的堆大小是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 的方式進行導出,這樣可以解決大量的內存。