在测试环境中开启的堆大小是4g。但是却发生了OOM。
发生OOM的场景是: 上传Excel 之后进行数据的清洗,然后清洗完成之后会将清洗掉的、清洗后的数据再次备份到磁盘中;同时将清洗后的数据入关系型数据库。(解析Excel 用的是POI, 数据清洗用的是Tablesaw, 且清洗的操作都是在内存中处理的)
记录下此次OOM的排查过程。
关于JVM调试的前置知识。
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.
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 模式。
(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 出的文件,导入之后界面如下:
查看类以及实例数量,判断占用内存多的对象:
场景是:POI 读取60M 50W 行Excel数据内存溢出。 这里解释下。 60M的excel 数据加载到JVM中转为List<Map> 结构就会占用1g的内存。
场景复现: 下面是自己main 方法启动复现场景。
设置内存溢出后生成dump 文件, 堆初始化和最大都是4g:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ -Xms4g -Xmx4g
也可以增加打印GC详细信息的参数
-XX:+PrintGCDetails
贴出来一段日志如下:
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)
(1) 拿到java_pid144064.hprof
(2) 用jvisualvm 装入后查看
(2) 查看类实例: 可以看到占用内存最大的类是char[] 和 String
poi 在读取过程中会一次性加载文件,然后转为字符串,最后会转char[] 数组对象,最终导致OOM。
最后采用阿里的easyexcel 进行读取避免了这个问题。
后记: 后来导出的时候也有类似的问题,最终是导出采用csv 的方式进行导出,这样可以解决大量的内存。