侧边栏壁纸
博主头像
敢敢雷博主等级

永言配命,自求多福

  • 累计撰写 57 篇文章
  • 累计创建 0 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

[转]OkHttp竟然玩出OOM?

敢敢雷
2021-10-17 / 0 评论 / 0 点赞 / 835 阅读 / 2,182 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我删除。

背景:最近上线的一个功能在运行一段时间后,系统触发了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的,根据名字应该是这个吧,打开里面一看

image

这个线程池的创建是写在静态域里面的,那就更不会有问题啊。看来还得从请求的源码开始追踪找线索。

我们从报错的日志从下往上看,第一行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请求网络,导致这是压死骆驼的最后一根稻草,但是这种情况肯定不会很频繁。

0

评论区