背景:最近上线的一个功能在运行一段时间后,系统触发了OOM告警。
按照经验,查看了JVM堆栈信息,发现内存几乎被OkHttp这个对象占满了。怀疑问题可能出在OkHttp上,于是先在网上搜索是否有类似的问题。幸运地找到了一篇很好的文章,对OkHttp源码进行了深入分析,转一手。
作者:键盘上的麒麟臂
链接:https://www.jianshu.com/p/3b232d9f38c2
来源:简书
我这使用okhttp短时间进行大量请求的时候会出现java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory的报错,毫无以为这就是溢出,我们熟悉的OOM。接着去看详细的信息。
java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:753)
java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:970)
java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1388)
okhttp3.Dispatcher.enqueue(Dispatcher.java:132)
okhttp3.RealCall.enqueue(RealCall.java:100)
一. 那么为什么okhttp会造成OOM
看到pthread_create就大概能猜到是线程的问题,应该是一个不断的创建线程所导致的。但是到这里我就觉得很奇怪,这样的网络请求框架应该是有线程池的啊,查看了源码,一看名字我就找到OkHttpClient里面有一个叫ConnectionPool的,根据名字应该是这个吧,打开里面一看
这个线程池的创建是写在静态域里面的,那就更不会有问题啊。看来还得从请求的源码开始追踪找线索。
我们从报错的日志从下往上看,第一行RealCall是在调newCall方法的时候创建的
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
然后你自然就能知道Call.enqueue就是这个RealCall的enqueue方法,找到它
public void enqueue(Callback responseCallback) {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.captureCallStackTrace();
this.eventListener.callStart(this);
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
看到this.client.dispatcher().enqueue就知道是调用OkHttpClient的Dispatcher的enqueue方法,找打Dispatcher
synchronized void enqueue(AsyncCall call) {
if (this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
this.runningAsyncCalls.add(call);
this.executorService().execute(call);
} else {
this.readyAsyncCalls.add(call);
}
}
看到了有引用线程池executorService,我们在这个类中看这个线程池相关的代码,AS能做搜索什么的操作,看源码还是挺方便的。
public synchronized ExecutorService executorService() {
if (this.executorService == null) {
this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
}
return this.executorService;
}
从这里可以看出每个okHttpClient对象在请求的时候都会创建一个线程池,而且线程池的keepAliveTime是1分钟
那么问题就找到了。我之前以为client表示连接,每个连接都应该是单独的对象,而且它使用的是Builder模式,所以我是在每次请求都去创建一个新的okHttpClient对象,所以会造成会new出一个新的线程池,那在1分钟之内大量进行请求(创建okHttpClient)的话当然会炸
解决的办法当然就是所有请求只使用同一个okHttpClient对象,使用单例模式之类的方法都可以解决。
二. okHttpClient设置属性的问题
那么问题又来了,我们使用单例,但是我上面说过,okHttpClient的创建是使用的Builder模式,那它的所有参数都是在Builder对象中传进去的,没有办法再创建完okHttpClient对象之后再去用setXXX方法去改参数。
举个栗子,我这个版本的设置请求超时时间是在okHttpClient中设置的
OkHttpClient.Builder okBuilder = new OkHttpClient.Builder();
okBuilder.connectTimeout(3000, TimeUnit.SECONDS);
OkHttpClient okHttpClient = okBuilder.build();
简单的写是这样,但是我每个请求都要求设置不同的请求时间怎么办,okHttpClient 只有一个对象,又没有setXXX方法。
去查找之后发现okHttpClient 有一个叫newBuilder的方法,这个方法就有意思的
public OkHttpClient.Builder newBuilder() {
return new OkHttpClient.Builder(this);
}
第一眼看这个方法,觉得就是重新创建一个OkHttpClient对象,实则另藏玄机
看到了没有,一个是new新的Dispatcher,一个是复用之前的Dispatcher,我们这里走的newBuilder就是调下面的那个方法,复用Dispatcher,那就不会创建新的线程池,就不会产生OOM。我也是第一次才知道,Builder模式还有这样的玩法
所以想要为某次请求改属性的时候可以这样写
okHttpClient().newBuilder().readTimeout(3000, TimeUnit.SECONDS).build().newCall(request);
三. 总结
使用OkHttp时,所有请求应使用同一个OkHttpClient,就是你不想使用同一个,也不能在短时间内大量创建。
OkHttpClient可以使用newBuilder的方法去更改OkHttpClient的属性。
四. 补充
补充问题:因为有朋友回复说还是会出现OOM,没关系,我们再进一步分析。
补充时间:2020.5.18
我们再来看一次源码
public void enqueue(Callback responseCallback) {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.captureCallStackTrace();
this.eventListener.callStart(this);
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
拿异步请求来举例,调用了OkHttpClient的Dispatcher的enqueue方法
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
@Nullable
private Runnable idleCallback;
@Nullable
private ExecutorService executorService;
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}
public Dispatcher() {
}
public synchronized ExecutorService executorService() {
if (this.executorService == null) {
this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
}
return this.executorService;
}
synchronized void enqueue(AsyncCall call) {
if (this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
this.runningAsyncCalls.add(call);
this.executorService().execute(call);
} else {
this.readyAsyncCalls.add(call);
}
}
}
(我把一些代码给屏蔽掉)可以看到它有一个线程池executorService,如果存在则返回,不存在则创建,也就是说这个线程池,一个OkHttpClient会创建一个,这个线程池的核心线程是0,并且没有队列,说明每有一个请求就会创建一个线程,而闲置60秒后就会回收这个线程。
那我们就肯定能得到一个答案,如果你每次请求都new OkHttpClient的话,就会每次都new ThreadPoolExecutor,短时间大量的请求会创建大量的线程,肯定会造成OOM
那为什么共用一个ThreadPoolExecutor就不会呢,短时间内大量请求依旧会创建大量线程,因为这个线程池的maxmumPoolSize是2147483647,这和不限制基本区别不大。
但是,这个Dispatcher对象,有两个队列,readyAsyncCalls和runningAsyncCalls。可以看到enqueue方法中。
if (this.runningAsyncCalls.size() < this.maxRequests) {
this.runningAsyncCalls.add(call);
} else {
this.readyAsyncCalls.add(call);
}
差不多是这个意思,如果runningAsyncCalls(正在运行异步的队列)长度为64的话,新添加进来的任务,就添加到readyAsyncCalls(准备异步队列)中。
为了测试效果,我写一个Demo并监测内存变化,写个死循环
while(true){
请求网络的操作......
}
跑了10分钟,最后发现一开始内存在缓慢的不断上升,当到达一定的时候,就不会上升了,我也使用抓包工具监测整个过程,一开始每有一个请求都会在列表中显示一条,速度较快,到后面,就开始变慢了,有时候同时请求3、4条,有时候只请求1条。
这说明什么?说明一开始runningAsyncCalls没达到64的时候一直在创建线程,所以内存会缓慢的上升,但是当runningAsyncCalls到达64之后,怎么说呢,就达到一种生产消费者模型,64是仓库的上限,满了就不生产了,所以内存最终会平稳在一个范围内。
结论:
所以说下为什么使用okhttp会导致OOM这个结论:
(1)创建了多个OkHttpClient,即便你使用okHttpClient的newBuilder方法,但是每次OkHttpClient都是new出来的,依旧会创建多个线程池,依旧会导致OOM。
(2)你的其它地方存在内存泄漏的情况或者内存已经接近爆满了,这时候你使用okhttp请求网络,导致这是压死骆驼的最后一根稻草,但是这种情况肯定不会很频繁。
评论区