一次Java进程物理内存占用超过最大堆内存分析
一、背景说明
项目测试过程中,测试人员反馈使用Excel导入100万号码时报错,通过Top命令查看到该java进程占用物理内存大小为3.4G,而且内存持续占用,一直没有释放,怀疑是不是有内存泄露的情况。
PS:上图为后续重新截的图,比第一次物理内存占用小
JVM配置参数如下:
server.java.opts=-d64 -XX:MaxPermSize=192M -Xms3000M -Xmx3000M -XX:+HeapDumpOnOutOfMemoryError
二、问题排查
查看日志发现出现了OOM
, 报错信息未:java.lang.OutOfMemoryError: GC overhead limit exceeded
,并生成了堆内存快照文件java_pid31734.hprof。
通过jVisualVM工具分析java_pid31734.hprof
文件。
java进程发生OOM时生成的堆内存快照文件大小为3.4G。
由于在项目中使用POI
去读取Excel文件的,从内存快照中可以看到大量的内存被POI
相关的对象占用。
发生OOM后java进程并没有退出,仍可以正常访问。接下来看一下Java进程的堆内存使用情况:
通过jmap
查看JVM堆内存使用总计约为152.8 MB:100.07(年轻代) + 52.73(老年代),说明出现OOM
后,占用的堆内存已经正常释放。
2.1 为什么堆内存释放后java进程占用的物理内存没有收缩?
项目启动后,该java进程占用内存也就1G多一点,进行导入Excel文件操作后,出现了OOM,但是后面堆内存已经正常释放,为什么该java进程占用物理内存没有释放?
操作系统内存分配机制
进程向操作系统申请内存时,操作系统分配给进程的是虚拟内存空间,只有进程真正访问这块内存时,操作系统才会给进程分配物理内存。
JVM启动时,向操作系统申请了3000M
的内存,但是这3000M
内存空间是虚拟内存空间,只有在java进程真正使用这些内存时操作系统才会去分配物理内。
由于JVM启动后,没有进行耗内存的操作,实际使用的内存空间较小,所以操作系统只分配真正使用到的物理内存给JVM,所以通过Top命令查看到的物理内存占用小于3000M
。
但是在处理Excel导入时发生了OOM,说明java进程已经使用了全部的堆内存,此时操作系统会将堆内存大小的物理内存全部分配给JVM。
上述JVM启动参数配置为-Xms3000M -Xmx3000M
, Xms
为最小堆内存,Xmx
为最大堆大小,由于这两个参数配置相同,所以JVM在堆内存空闲时仍不会归还物理内存给操作系统。
可以通过pmap
命令看一下进程内存占用情况:
从上图可以看出堆内存释放后,JVM底层并未将物理内存归还给操作系统。
2.2 为什么java进程占用的内存大小超过了堆内存大小?
JVM 的内存大概分为下面这几个部分
堆(Heap):eden、survivor、old 区域等
线程栈(Thread Stack):每个线程栈预留 1M 的线程栈大小
非堆(Non-heap):包括 code_cache、metaspace 等
堆外内存:unsafe.allocateMemory 和 DirectByteBuffer申请的堆外内存
native (C/C++ 代码)申请的内存
还有 JVM 运行本身需要的内存,比如 GC 等。
除去JVM堆内存外,java进程本身会占用一些内存,所以会出现java进程的占用的总内存会比最大堆内存大的情况。
三、POI读取超大Excel文件OOM问题
POI
是读写Excel的常用工具包,功能非常丰富。但是POI
的缺点也非常明显,在导入100万手机号码的Excel文件时,即使最大堆设置为4000M
也还是会发生OOM的。
所以项目后续采用了阿里开源的easyexcel
,easyexcel
官网给出的数据为:64M内存1分钟内读取75M(46W行25列)的Excel。
使用easyexcel
后成功解决读取百万以上Excel数据OOM问题。
参考文章
一次 Java 进程 OOM 的排查分析(glibc 篇)
记一次堆外内存泄漏排查过程
Understanding glibc malloc