Java OOM导致coredump

最近发现一个Java进程每隔几天就死掉一次,第一反应是Java进程有内存泄漏,果断安装JDK,通过jvisualvm监控内存占用,果然发现问题:

  1. 活动线程数一直上涨,必然存在问题
  2. 每隔一段时间就会启动大量线程,大约有几百个之多

修改第一个问题后,同时缩短轮询周期(因为以上两个问题都是在触发轮询的时候出现)继续监控,活动线程数稳定下来了,但还是会死掉。
是Java内存没有及时触发内存回收,而分配内存的时候出现大对象导致内存无法申请?设置虚拟机参数:

-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly

问题依旧存在。继续分析发现Java进程死掉的时候内存占用远没有达到设置的堆内存上限4G(-Xmx4130m),此时开始怀疑引发问题的原因不是Java进程自身占用内存超限而是系统的内存不够用。此时找到hs_err_pidxxxx.log,其内容如下:

#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 670711808 bytes for committing reserved memory.
# Possible reasons:
#   The system is out of physical RAM or swap space
#   In 32 bit mode, the process size limit was hit
# Possible solutions:
#   Reduce memory load on the system
#   Increase physical memory or swap space
#   Check if swap backing store is full
#   Use 64 bit Java on a 64 bit OS
#   Decrease Java heap size (-Xmx/-Xms)
#   Decrease number of Java threads
#   Decrease Java thread stack sizes (-Xss)
#   Set larger code cache with -XX:ReservedCodeCacheSize=
# This output file may be truncated or incomplete.
#
#  Out of Memory Error (os_linux.cpp:2760), pid=8483, tid=0x00007efe60cf0700
#
# JRE version: OpenJDK Runtime Environment (8.0_191-b12) (build 1.8.0_191-b12)
# Java VM: OpenJDK 64-Bit Server VM (25.191-b12 mixed mode linux-amd64 compressed oops)
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
......

进一步确认了是系统内存不够用了。此时想到的解决办法有两个:
1、 开源:增加内存或者swap
2、 节流:找到消耗内存的进程,减少内存消耗

增加4G swap,果然问题有所缓解,几天没有出现coredump的问题,但这治标不治本,系统内存配置了8G,Java配置堆内存4G,其他的内存哪去了?

通过top观察,发现MongoDB占用了大量内存,并且增长很快,在一天之内就从不到100M涨到了3.5G,剩余内存只有几百M:这就可以解释了,内存都被MongoDB占去了,Java进程在申请内存的时候自然就失败导致coredump了。
限制MongoDB缓存大小为512M:

  engine: wiredTiger
#  mmapv1:
  wiredTiger:
    engineConfig:
      configString : cache_size=512M

继续观察,果然MongoDB内存稳定在500M左右,运行一个星期Java进程也没有coredump过

2019-08-09 13:59:08		1564		1486432		536792		4.3		36		66
2019-08-09 14:04:07		1564		1486432		536936		3.7		36		66
2019-08-09 14:09:08		1564		1486432		536936		4.3		36		66
......

但事情还没完,MongoDB的内存是稳定了,但Java进程占用的内存怎么还是一直涨,甚至超过了4G?

2019-08-06 17:47:05             1046            7382972         571036          417.7           22              57
2019-08-06 17:48:04             1046            9.951g          1.394g          211.0           257             1107
2019-08-06 17:49:05             1046            10.037g         1.761g          10.7            326             1251
2019-08-06 17:50:05             1046            10.106g         2.878g          278.1           375             1264
2019-08-06 17:51:05             1046            10.108g         3.242g          84.7            370             1277
2019-08-06 17:52:04             1046            10.109g         3.454g          153.8           364             1260
2019-08-06 17:53:05             1046            10.110g         3.637g          29.9            360             1257
......
2019-08-06 18:00:04             1046            10.112g         4.170g          63.0            367             1261
......
2019-08-06 18:57:04             1046            10.117g         4.306g          2.7             359             1276
......
2019-08-06 23:58:05             1046            10.246g         4.403g          9.0             362             1263
......
2019-08-07 05:04:04             1046            10.312g         4.636g          4.3             360             1299
......
2019-08-07 08:08:04             1046            10.312g         4.727g          7.0             362             1306
......
2019-08-07 09:24:04             1046            10.375g         4.739g          4.3             360             1289
......
2019-08-07 13:35:04             1046            10.375g         4.909g          14.3            364             1276
.......

原来Java进程-Xmx4130m所设置的仅仅是堆内存,除了堆内存之外,Java进程占用的内存还包括:

  • 栈内存
  • 代码段占用内存
  • DirectByteBuffer所占用的堆外内存

其中DirectByteBuffer所占用的堆外内存,常被称作“冰山对象”。

  • 为什么要使用堆外内存

DirectByteBuffer在创建的时候会通过Unsafe的native方法来直接使用malloc分配一块内存,这块内存是heap之外的,那么自然也不会对gc造成什么影响(System.gc除外),因为gc耗时的操作主要是操作heap之内的对象,对这块内存的操作也是直接通过Unsafe的native方法来操作的,相当于DirectByteBuffer仅仅是一个壳,还有我们通信过程中如果数据是在Heap里的,最终也还是会copy一份到堆外,然后再进行发送,所以为什么不直接使用堆外内存呢。对于需要频繁操作的内存,并且仅仅是临时存在一会的,都建议使用堆外内存,并且做成缓冲池,不断循环利用这块内存。

  • 为什么不能大面积使用堆外内存

如果我们大面积使用堆外内存并且没有限制,那迟早会导致内存溢出,毕竟程序是跑在一台资源受限的机器上,因为这块内存的回收不是你直接能控制的,当然你可以通过别的一些途径,比如反射,直接使用Unsafe接口等,但是这些务必给你带来了一些烦恼,Java与生俱来的优势被你完全抛弃了—开发不需要关注内存的回收,由gc算法自动去实现。另外上面的gc机制与堆外内存的关系也说了,如果一直触发不了cms gc或者full gc,那么后果可能很严重。

可以通过如下设置限制堆外内存的使用:

-XX:MaxDirectMemorySize=512M

参考

  1. Linux下创建、销毁、使用 SWAP
  2. 关于mongodb占用内存过大的问题
  3. mongodb 3.2配置内存缓存大小为MB
  4. 进程物理内存远大于Xmx导致堆未满但OOME
  5. JVM源码分析之堆外内存完全解读
  6. MaxDirectMemorySize的设置
  7. Java堆外内存之七:JVM NativeMemoryTracking 分析堆外内存泄露

你可能感兴趣的:(Java,OOM,Java)