【JVM】Out of Memory 内存溢出排查

上周做了在定时任务服务上做了一个缓存预热功能,周五发到了测试环境,周一来发现竟然OOM,原因大概是不能再创建线程之类的。

然后从grafana看了下这几天的内存趋势图,发现果然一直在涨。

开始排查。

如果你的pod启动命令添加了:XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/logs/HeapDump_Gc/${HOSTNAME}.hprof

联系运维把dump文件拉下来,用工具分析。

不幸的事,我进入到该pod并没有生成dump文件,只好换另外一种方式。

换个思路,本地启动,用arthas监控。 

本地跑项目,启动arthas抓dump日志,放到jProfiler工具里分析(JDK自带的也可以)。

对比发现,无特殊重大对象。

后来,在pod中执行 top -H -p 进程id 。【查看一个进程创建的线程数】

发现项目进程竟创建了2000多个线程。

怎么排查是什么线程?

也用jProfiler工具。

可以看到,手动GC,是可以回收的。

因此又去看了下实时的线程,发现有一个定时任务线程不断增加,且一直未被回收(一时太激动,忘了截图了)。

看来是因为创建太多线程,达到线程创建上限了(至于为什么线程不停在创建,甚至内存都快满了,也没有触发垃圾回收,这里还有待研究)。

后经过排查发现是下面这个定时任务线程池,每启动一个任务,就创建一个线程,且没有触发垃圾回收,因此就线程创建上限,OOM了。

 

解决办法,手动指定定时任务线程池:

@Bean
public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
 scheduler.setPoolSize(16);
 return scheduler;
}

关于为什么会不听创建线程,详情可参考@EnableAsync注解的使用:SpringBoot中并发定时任务的实现、动态定时任务的实现(看这一篇就够了)- 每日博客

本项目正是由于误用异步线程:

  1. 误把每个定时任务类上都加上@EnableAsync注解,其实不用,只需要在启动类加即可。
  2. Spring默认线程池为了兼容并发性,设置并发线程无线创建,而旧的线程又没有触发GC,没有及时回收,所以达到进程创建线程上限,报OOM异常。

所以,这项目前人还是留下很多坑的,要多注意!

加餐:

关于一个进程最多可创建多少线程?

可参考:一个进程最多可以创建多少个线程? - 知乎

引用:

那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是我们可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。

pod栈空间为8g。

64 位系统的虚拟内存大小,理论上可以创建无数个线程。

事实上,肯定创建不了那么多线程,除了虚拟内存的限制,还有系统的限制。

比如下面这三个内核参数的大小,都会影响创建线程的上限:

  • /proc/sys/kernel/threads-max,表示系统支持的最大线程数,默认值是 14553
  • /proc/sys/kernel/pid_max,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是 32768
  • /proc/sys/vm/max_map_count,表示限制一个进程可以拥有的VMA(虚拟内存区域)的数量,具体什么意思我也没搞清楚,反正如果它的值很小,也会导致创建线程失败,默认值是 65530

从这几个参数上看,能创建上万个线程,那么限制出现在哪里了呢?

看一下Pod的可用内存大小: 

看下JVM的启动参数设置:

java -jar -Dcom.sun.management.jmxremote.authenticate=false 非认证登录

-Dcom.sun.management.jmxremote.port=1899

-Dcom.sun.management.jmxremote.ssl=false

-Xms512m  堆初始内存 -Xmx7048m 堆对大内存  -Xmn1024m 年轻代大小   -Xss1m 每个线程的堆栈大小(重点在这里,1.8g内存,每个1m,差不多能创建2000个左右)

-XX:MetaspaceSize=128m

-XX:MaxMetaspaceSize=512m

-XX:+UseCompressedOops

-XX:+UseConcMarkSweepGC

-XX:CMSInitiatingOccupancyFraction=75

-XX:+UseCMSInitiatingOccupancyOnly

-XX:MaxTenuringThreshold=6

-XX:+ExplicitGCInvokesConcurrent

-XX:+ParallelRefProcEnabled

-Xloggc:/home/logs/HeapDump_Gc/${HOSTNAME}-gc.log

-XX:+PrintGCDateStamps -XX:+PrintGCDetails

-XX:+PrintGCApplicationStoppedTime

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/home/logs/HeapDump_Gc/${HOSTNAME}.hprof

-Dfile.encoding=UTF-8

-Duser.timezone=GMT+08

-Dspring.profiles.active=${profile}  

后续:

修改之后,可以看到正常允许,没有OOM产生,同时达到一定上限也会触发GC回收了。

关于springBoot的线程池:

 

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值