最近发现一个Java进程每隔几天就死掉一次,第一反应是Java进程有内存泄漏,果断安装JDK,通过jvisualvm监控内存占用,果然发现问题:
- 活动线程数一直上涨,必然存在问题
- 每隔一段时间就会启动大量线程,大约有几百个之多
修改第一个问题后,同时缩短轮询周期(因为以上两个问题都是在触发轮询的时候出现)继续监控,活动线程数稳定下来了,但还是会死掉。
是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