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