线上服务内存溢出
这周刚下班忽然有一个我的项目内存溢出了,排查了半天终于找到问题所在,在此记录下,避免前面再次出现相似的状况。
先简略说下当呈现内存溢出之后,我是如何排查的,首先通过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...
更多精彩内容请关注微信公众号:一个程序员的成长