OkHttp竟然玩出OOM?

我这使用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)

最终的错误指向到okhttp3

一. 那么为什么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);

三. 总结

  1. 使用OkHttp时,所有请求应使用同一个OkHttpClient,就是你不想使用同一个,也不能在短时间内大量创建。
  2. 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请求网络,导致这是压死骆驼的最后一根稻草,但是这种情况肯定不会很频繁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容