乐趣区

关于java:Executors使用不当引起的内存溢出

线上服务内存溢出

这周刚下班忽然有一个我的项目内存溢出了,排查了半天终于找到问题所在,在此记录下,避免前面再次出现相似的状况。

先简略说下当呈现内存溢出之后,我是如何排查的,首先通过 jstack 打印出堆栈信息,而后通过剖析工具对这些文件进行剖析,依据剖析后果咱们就能够晓得大略是因为什么问题引起的。

对于 jstack 如何应用,大家能够先看看这篇文章 jstack 的应用

问题排查

上面是我打印进去的信息,大部分都是这个

"http-nio-8761-exec-124" #580 daemon prio=5 os_prio=0 tid=0x00007fbd980c0800 nid=0x249 waiting on condition [0x00007fbcf09c8000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000f73a4508> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
        at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
        at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:85)
        at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:31)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

看到了如上信息之后,大略能够看出是因为线程池的使用不当导致的,那么依据信息持续往下看,看到 ThreadPoolExecutor 那么就能够晓得这必定是创立了线程池,那么咱们就在代码里找,哪里创立应用了线程池,我就找到这么一段代码。

public class ThreadPool {
    private static ExecutorService pool;

    private static long logTime = 0;

    public static ExecutorService getPool() {if (pool == null) {pool = Executors.newFixedThreadPool(20);
        }
        return pool;
    }
}

乍一看,可能写的同学是想把这当一个全局的线程池用,所有的业务但凡用到线程的都会应用这个类,为了对立治理线程,想法没什么故障,然而这样写的确有点子故障。

newFixedThreadPool 剖析

下面应用了 Executors.newFixedThreadPool(20)创立了一个固定的线程池,咱们先剖析下 newFixedThreadPool 是怎么样的一个流程。

一个申请进来之后,如果外围线程有闲暇线程间接应用外围线程中的线程执行工作,不会增加到阻塞队列中,如果外围线程满了,新的工作会增加到阻塞队列,直到队列加满再开线程,直到 maxPoolSize 之后再触发拒绝执行策略

理解了流程之后咱们再来看 newFixedThreadPool 的代码实现。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {if (capacity <= 0) throw new IllegalArgumentException();
    // 工作阻塞队列的初始容量
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

定位问题

看到了这里不晓得你是否晓得了此次引起内存透露的起因,其实就是因为 阻塞队列的容量过大

如果不手动的指定阻塞队列的大小,那么它默认是 Integer.MAX_VALUE,咱们的线程池只有 20 个线程能够解决工作,其余的申请全副放到阻塞队列中,那么当涌入大量的申请之后,阻塞队列始终减少,你的内存配置又十分紧凑的话,那么是很容易呈现内存溢出的。

咱们的业务是在 APP 启动的时候,会应用线程池去检查用户的一些配置,利用的启动量还是十分大的而且给的内存配置也不是很足,所以运行一段时间后,局部容器就呈现了内存溢出的状况。

如何正确的创立线程池

以前其实没太在意这种问题,都是应用 Executors 去创立线程,然而这样的确会存在一些问题,就像这些的内存透露,所以个别不要应用 Executors 去创立线程,应用 ThreadPoolExecutor 进行创立,其实 Executors 底层也是应用 ThreadPoolExecutor 进行创立的。

应用 ThreadPoolExecutor 创立须要本人指定外围线程数、最大线程数、线程的闲暇时长以及阻塞队列。

3 种阻塞队列
  • ArrayBlockingQueue:基于数组的先进先出队列,有界
  • LinkedBlockingQueue:基于链表的先进先出队列,有界
  • SynchronousQueue:无缓冲的期待队列,无界

咱们应用了有界的队列,那么当队列满了之后如何解决前面进入的申请,咱们能够通过不同的策略进行设置。

4 种回绝策略
  • AbortPolicy:默认,队列满了丢工作抛出异样
  • DiscardPolicy:队列满了丢工作不异样
  • DiscardOldestPolicy:将最早进入队列的工作删,之后再尝试退出队列
  • CallerRunsPolicy:如果增加到线程池失败,那么主线程会本人去执行该工作

在创立之前,先说下我最开始的版本,因为队列是固定的,最开始咱们不晓得有回绝策略,所以在队列满了之后再增加的话会出现异常,我就在异样外面睡眠了 1 秒,期待其余的线程执行结束获取闲暇连贯,然而还是会有局部不能失去执行。

接下来咱们来创立一个容错率比拟高的线程池。

public class WordTest {public static void main(String[] args) throws InterruptedException {System.out.println("开始执行");

        // 阻塞队列容量申明为 100 个
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));

        // 设置回绝策略
        executorService.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 闲暇队列存活工夫
        executorService.setKeepAliveTime(20, TimeUnit.SECONDS);

        List<Integer> list = new ArrayList<>(2000);

        try {
            // 模仿 200 个申请
            for (int i = 0; i < 200; i++) {
                final int num = i;
                executorService.execute(() -> {System.out.println(Thread.currentThread().getName() + "- 后果:" + num);
                    list.add(num);
                });
            }
        } finally {executorService.shutdown();
            executorService.awaitTermination(10, TimeUnit.SECONDS);
        }
        System.out.println("线程执行完结");
    }
}

思路:我申明了 100 容量的阻塞队列,模仿了一个 200 的申请,很显然必定有局部申请进入不了队列,然而我应用了 CallerRunsPolicy 策略,当队列满了之后,应用主线程去进行解决,这样就不会呈现有局部申请得不到执行的状况,也不会因为因为阻塞队列过大导致内存溢出的状况。

如果还有什么更好地写法欢送各位指教!

通过测试 200 个申请全副失去执行,有 3 个申请由主线程进行了解决。

总结

如何更好的创立线程池下面曾经说过了,对于线程池在业务中的应用,其实咱们这种全局的思路是不太好的,因为如果从全局思考去创立线程池,是很难把控的,因为你无奈精确地评估所有的申请加起来会有多大的量,所以最好是每个业务创立独立的线程池进行解决,这样是很容易评估量化的。

另外创立的时候,最好评估下大略每秒的申请量有多少,而后来正当的初始化线程数和队列大小。

参考文章:<br/>
https://www.cnblogs.com/muxi0…

更多精彩内容请关注微信公众号:一个程序员的成长

退出移动版