关于RxJava使用时出现OOM问题的调查和解决
最近项目内开始使用Retrofit和RxJava大面积替换原有的网络请求框架,从易用性上有了大幅的改观,但是上线后,出现了大量内存溢出问题。
报错信息
新版本上线后,后台报错信息被OOM问题刷屏,具体报错信息类似于:Caused by: java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
。从字面上推断,基本上就是因为内存占用过多,线程申请失败。
原因分析
新版本中因为含有不少重要功能,做了很多改动,但是涉及到线程调度的修改,基本只有RxJava这里。翻看了RxJava的相关文档,发现可疑之处。如果没有特意设置,那么RxJava会使用自带的线程池进行线程调度管理。其中的io线程池,是一个无上限限制的线程池,只要需要,就会立刻申请新的线程,线程使用完成,如果一直处于闲置状态,会在1min后释放。如果出现大量的线程申请,就是io线程池的问题。
问题追踪
定位到这里,我们开始着手实验。运行APP,打开调试模式,在请求量相对较大的几个页面来回切换,观测调试面板中的线程情况。但是没有发现任何异常,io线程数基本维持在4个以下,属于正常水平,静置一分钟后,空闲线程被释放,只保留了一个核心线程。
不能够复现,只能够使用Google大法了,一番Google后,遇到类似问题的大有人在。而且更幸运的是,Git上直接有人找RxAndroid和Retrofit作者JakeWharton反馈这个问题。大神不愧是大神,回复的也非常干脆利落,老子代码没问题,肯定是你自己使用的有问题!其他有用的信息基本没有了,就此Google大法也没用了。
思考了一下我的代码和Jake代码出问题的概率,我觉得还是继续找自己的锅更靠谱一些。此时测试同学传来好消息,能大概率复现了,以往都是躲着QA的同学们走,这次突然发现他们变得可爱了不少,哈哈。按照测试同学指定额复现路径,进行操作,发现io线程数在不断上涨,然而线程池里一堆wait的线程却不重复利用,也不释放,线程涨到了40个左右的时候,APP挂掉了。在过程中查看了APP的内存使用,没有发现异常增长,代码中的内存泄露检测工具也么有发出任何警报,神奇!
,基本确定了是io线程池申请了线程没有释放导致的,那么我们就要考虑到底是什么原因导致这种现象了。一般情况上来说,如果线程池中有wait的线程,线程池会直接使用它,而不用重新创建。这也是我们使用线程池的原因。查看RxJava的源码,io线程池的类为CachedWorkerPool
,其内部维护了一个阻塞队列,用于记录所有可用线程,当有新的线程需求时,线程池会查询阻塞队列中是否有可用线程,没有的话就新建一个。结合这里我们要看看所有使用过的那些空闲线程为什么没有加入到阻塞队列中去就可以了。
关键代码如下:
1 | void release(ThreadWorker threadWorker) { |
调用此处代码的入口也很少,只有一处:
1 | @Override |
对于这一处的调用,可以简单理解为线程内部维护了一个状态列表,当线程内的任务完成之后,会调用call来释放线程的占用,到此处,我们就基本明白问题在哪里了。肯定是有些地方的Observable没有结束,所以一直占用着线程。出现这种问题有以下几种情况:
- 网络请求非常慢,导致大量的请求堆积;
- 网络请求完成后,有偶发性的耗时操作,放在了io线程中处理;
- 在自己创建的Observable中,没有调用正常的生命周期;
结合测试提供的复现路径,很快定位到了问题出在了3上,有一处数据读取,在OnSubscribe中,之调用了Subscriber的onNext,没有调用onComplete,从而Subscriber没有取消订阅,虽然业务功能能够正常完成,CPU和内存的使用也没有明显异常,但是造成了线程的占用,无法正常释放,从而导致了OOM。
解决方案
在OnSubscribe中,调用Subscriber的onComplete。
一行代码搞定了困扰了好长时间的BUG,也充分暴露了自己对于RxJava的掌握还太过肤浅,费了这么多的周折,才定位到原因。如果大家也有人遇到了同样的问题,那么第一步要做的就是,查一下onComplete到底调用了没。ORZ