cpu占用率高排查

  1. top查出哪个java进程最消耗cpu命令:top 如下图

image.png
这三个指标可以看出进程PID为87010的进程很消耗资源

  1. 根据进程87010查出哪个线程最消耗cpu命令:top -Hp 87010 如下图

image.png
通过上图三个指标可以看出线程90255比较消耗资源,TIME列就是各个Java线程耗费的CPU时间。

  1. printf “%x\n” 90255 得到一个16进制的数值 1608f
  2. jstack 87010|grep 1608f,它用来输出进程90255的堆栈信息,然后根据线程ID的十六进制值grep,如下:

image.png
可以看出是线程池ThreadPoolExecutor的问题,ok问题定位到了。

OutOfMemoryError: Java heap space 问题分析&解决方案

前言
生产环境OOM并不可怕,可怕的是你不知道问题所在,一直在扩大运行内存。楼主的生产环境OOM异常如图:

分析手段
首先需要做的是为运行环境加上gc日志&在内存溢出的时候让他产生一个内存快照。
我是tomcat运行的服务所以去改tomcat启动参数:
找到对应运行服务的tomcat/bin目录修改启动参数如下图

参数 作用
-XX:MaxPermSize=4096M 永久代大小调整
-XX:+PrintGCDetails 开启了GC日志输出
-XX:+PrintGCDateStamps 输出格式
-XX:+PrintGCTimeStamps 输出格式
-Xloggc:/home/apache-tomcat-web/logs/gc.$$.log 指定GC log的位置,以文件输出
-XX:+HeapDumpOnOutOfMemoryError 快照保存
-XX:HeapDumpPath=/home/apache-tomcat-web/logs/ 快照保存地址
PS:jvm性能调优是一个很复杂的东西,涉及到了很多东西,一般情况下大多数都是代码的问题引起的OOM异常,我们这里先附上一篇楼主认为写的比较好的博客,方便理解学习。

深入理解JVM原理:https://blog.csdn.net/know9163/article/details/80574488

冷静对待你遇到的所有Java内存异常:https://zazalu.space/2019/09/17/java-memory-error-solution-Theoretically/

言归正传,我们在启动了上面的参数后,在程序再一次产生OOM异常的时候,我们可以去/home/apache-tomcat-web/logs/目录下获取当时产生的内存快照如下图:
PS:gc日志是留着JVM调优用的

把这个快照文件从服务器上拿到本地电脑,然后用java自带的jvisualvm.exe分析
导入快照文件:
查看结果:
PS如果你跟我的问题不一样,可以去看看类找到占用最多的进行分析。
看类信息:
楼主这里基本已经定位问题了,

再结合jstack找出最耗cpu的线程:
https://blog.csdn.net/shasiqq/article/details/109801683

基本可以看出是Spring session(redis存储方式)监听导致创建大量redisMessageListenerContailner-X线程

解决办法
为spring session添加springSessionRedisTaskExecutor线程池。

Spring boot方式

  1. /**
  2. * 用于spring session,防止每次创建一个线程
  3. * @return
  4. */
  5. @Bean
  6. public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
  7. ThreadPoolTaskExecutor threadPoolTaskExecutor= new ThreadPoolTaskExecutor();
  8. threadPoolTaskExecutor.setCorePoolSize(8);
  9. threadPoolTaskExecutor.setMaxPoolSize(16);
  10. threadPoolTaskExecutor.setKeepAliveSeconds(10);
  11. threadPoolTaskExecutor.setQueueCapacity(1000);
  12. threadPoolTaskExecutor.setThreadNamePrefix("Spring session redis executor thread: ");
  13. return threadPoolTaskExecutor;
  14. }

spring xml配置方式:

  1. <bean id="threadPoolTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  2. <property name="corePoolSize" value="8" />
  3. <property name="maxPoolSize" value="16" />
  4. <property name="keepAliveSeconds" value="10" />
  5. <property name="queueCapacity" value="1000" />
  6. </bean>

原因
在Spring Session(redis)的配置类源码中(RedisHttpSessionConfiguration):

  1. @Autowired(
  2. required = false //该处理监听的线程池不是必须的,如果不自定义默认将使用SimpleAsyncTaskExecutor线程池
  3. )
  4. @Qualifier("springSessionRedisTaskExecutor")
  5. public void setRedisTaskExecutor(Executor redisTaskExecutor) {
  6. this.redisTaskExecutor = redisTaskExecutor;
  7. }

springSessionRedisTaskExecutor不是必须的,如果不自定义则spring默认将使用SimpleAsyncTaskExecutor线程池。

题外话
SimpleAsyncTaskExecutor:每次都将创建新的线程(说是“线程池”,其实并非真正的池化,但它可以设置最大并发线程数量。)
@EnableAsync开启异步方法,背后默认使用的就是这个线程池。使用异步方法时如果业务场景存在频繁的调用(该异步方法),请自定义线程池,以防止频繁创建线程导致的性能消耗。如果该异步方法存在阻塞的情况,又调用量大,注意有可能导致OOM(线程还未结束,又增加了更多的线程,最后导致内存溢出)。@Async注解可以选择使用自定义线程池。
它创建了SimpleAsyncTaskExecutor

说回RedisHttpSessionConfiguration,我们接着看:

  1. @Bean
  2. public RedisMessageListenerContainer redisMessageListenerContainer() {
  3. RedisMessageListenerContainer container = new RedisMessageListenerContainer();
  4. container.setConnectionFactory(this.redisConnectionFactory);
  5. if (this.redisTaskExecutor != null) {
  6. container.setTaskExecutor(this.redisTaskExecutor);
  7. }
  8. if (this.redisSubscriptionExecutor != null) {
  9. container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
  10. }
  11. container.addMessageListener(this.sessionRepository(), Arrays.asList(new PatternTopic("__keyevent@*:del"), new PatternTopic("__keyevent@*:expired")));
  12. container.addMessageListener(this.sessionRepository(), Collections.singletonList(new PatternTopic(this.sessionRepository().getSessionCreatedChannelPrefix() + "*")));
  13. return container;
  14. }

RedisMessageListenerContainer正是处理监听的类,RedisMessageListenerContainer设置了不为空的redisTaskExecutor,因为spring session默认没有配置该Executor,那RedisMessageListenerContainer在处理监听时怎么使用线程呢?我们接着看RedisMessageListenerContainer的源码:

  1. public void afterPropertiesSet() {
  2. if (this.taskExecutor == null) {
  3. this.manageExecutor = true;
  4. this.taskExecutor = this.createDefaultTaskExecutor();
  5. }
  6. if (this.subscriptionExecutor == null) {
  7. this.subscriptionExecutor = this.taskExecutor;
  8. }
  9. this.initialized = true;
  10. }
  11. protected TaskExecutor createDefaultTaskExecutor() {
  12. String threadNamePrefix = this.beanName != null ? this.beanName + "-" : DEFAULT_THREAD_NAME_PREFIX;
  13. return new SimpleAsyncTaskExecutor(threadNamePrefix);
  14. }

afterPropertiesSet()这个方法熟悉吧,这个方法将在所有的属性被初始化后调用(InitializingBean接口细节这里不再赘述)。
所以如果用户没有定义springSessionRedisTaskExecutor,Spring session将调用createDefaultTaskExecutor()方法创建SimpleAsyncTaskExecutor线程池。而这个“线程池”处理任务时每次都创建新的线程。所以你会发现很多个redisMessageListenerContailner-X线程。