
首發(fā)于公眾號: DSGtalk1989
好久不見,值此年終之際,跟大家探討一下,一個詭異的內(nèi)存溢出。
羈絆與猜想
話不多說,先上崩潰
java.lang.OutOfMemoryError
pthread_create (1040KB stack) failed: Out of memory
解析原始
1 java.lang.Thread.nativeCreate(Native Method)
2 java.lang.Thread.start(Thread.java:1078)
很顯然,創(chuàng)建了一個線程,1040kb,內(nèi)存溢出。
正好我們比對一下一般的內(nèi)存溢出的情況是怎樣的。
java.lang.OutOfMemoryError
Failed to allocate a 1056012 byte allocation with 801854 free bytes and 783KB until OOM
當(dāng)再次分配內(nèi)存的時候出現(xiàn)了內(nèi)存溢出。
那么我會猜測以上兩種情況是否都是內(nèi)存不夠用了,或者說上面的是否和下面的一樣是因為內(nèi)存不夠用了引起的。
為什么恰好的創(chuàng)建線程的時候崩潰了,創(chuàng)建線程需要的內(nèi)存通常不會很大,就真的那么巧,這一根稻草壓垮了駱駝?
一筆帶過的結(jié)論
在此直接吐結(jié)論,因為探索pthread_create的內(nèi)存溢出原因,不是本文的主旨,本文主要探討是如何斗爭。
大家應(yīng)該都知道,線程創(chuàng)建的越多,資源就消耗的越大,android本身并沒有為我們限制應(yīng)用承受的最大線程數(shù),將控制權(quán)交由應(yīng)用設(shè)計者自己。
那么很明顯國內(nèi)的廠商,并不相信國內(nèi)的開發(fā),或者說國內(nèi)的廠商由于自己的定制導(dǎo)致你即使遵循了應(yīng)用的開發(fā)之道,卻依然難以避免產(chǎn)生大量的冗余線程。
因此華為帶頭出擊,魅族隨后,分別將自己的大量機型可控線程數(shù)調(diào)至500和3000。
如果你深表懷疑或者依然存有困惑,送你如下傳送門
仗怎么打
首先我們需要將應(yīng)用中可能出現(xiàn)的所有與線程相關(guān)的地方找到,或者說找出應(yīng)用中可能會出現(xiàn)線程池的地方。
此處只舉個例,不做窮舉。
網(wǎng)絡(luò)請求肯定是首當(dāng)其沖,如果你使用的市面流行的第三方框架,
Retrofit+Okhttp肯定有默認的線程池問題,如果你在外面還包了一層Rxjava,那么在這一層面還有一個線程池問題。
當(dāng)然,如果牛逼到自己封裝了自己的一套網(wǎng)絡(luò)請求,那么想必會考慮到多線程的問題,應(yīng)該如何挑選最合適的線程池來讓自己的效率最優(yōu)。項目是否使用了事件總線,同上,如果你使用了
EventBus,那么這玩意兒幫我們使用了默認的線程池,如果你同樣自定義了一套完整的事件總線,那么你也同樣需要關(guān)注多線程的問題。如果項目較大,很有可能還會有自定義的
Executor,全局搜索一下,看下是如何定義的線程池。
再看Executor
啥都不說,我們從構(gòu)造方法開始
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)
-
corePoolSize
the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set線程池中所保存的核心線程數(shù),包括空閑線程。除非設(shè)置了
allowCoreThreadTimeOut屬性我們可以直接理解成,當(dāng)緩沖隊列中為空時,線程池中最大允許的線程數(shù)量,而且此時,只要有一個任務(wù)過來就會新建一個線程,直到達到核心線程數(shù)。
-
maximumPoolSize
the maximum number of threads to allow in the pool線程池中允許的最大線程數(shù)
你可以理解成線程池中允許的最大同時執(zhí)行線程數(shù) -
keepAliveTime
when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.當(dāng)線程池中的線程數(shù)比核心線程數(shù)來的多時,空閑線程所能持續(xù)的最長時間。
這個講的比較明確了。 -
unit
the time unit for the {@code keepAliveTime} argument時間單位
-
workQueue
the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.任務(wù)執(zhí)行前保存任務(wù)的隊列,僅保存由 execute 方法提交的 Runnable 任務(wù)。
-
handler
the handler to use when execution is blocked because the thread bounds and queue capacities are reached當(dāng)線程越界,且隊列阻塞了之后的處理類。
捋順Executor
當(dāng)來了一個新的任務(wù)
- 我們會首先去看線程池中是否有空閑的線程
- 如果線程池中當(dāng)前沒有線程,創(chuàng)建一個線程執(zhí)行任務(wù)
- 如果當(dāng)前線程池中線程小于核心線程數(shù),創(chuàng)建一個線程執(zhí)行任務(wù)
- 如果當(dāng)前線程池中線程數(shù)等于核心線程數(shù),將任務(wù)放入緩存隊列,等待線程執(zhí)行完畢,就將任務(wù)拋到執(zhí)行完任務(wù)的線程執(zhí)行。
- 如果當(dāng)前線程池中線程數(shù)等于核心線程數(shù),且緩存隊列已滿,且當(dāng)前線程池內(nèi)的線程數(shù)量小于最大線程數(shù),那么就創(chuàng)建一個線程出來執(zhí)行任務(wù)
- 如果當(dāng)前線程池線程數(shù)等于最大線程數(shù),那么會觸發(fā)handler,詢問該如何處理超過的線程數(shù),一旦沒有創(chuàng)建handler,handler不作處理,則直接拋出未處理異常。
總結(jié)來說就是 核心 > 隊列 > 非核心 > handler
拿Retrofit開刀
OK,當(dāng)我們真的搞清楚了,到底Executor是怎么一回事了之后,我們來拿Retrofit1的RestAdapter看一下。
進到RestAdapter的內(nèi)部類Builder類。
public static class Builder {
...
private void ensureSaneDefaults() {
...
if(this.httpExecutor == null) {
this.httpExecutor = Platform.get().defaultHttpExecutor();
}
}
}
如果你不去專門設(shè)置httpExecutor,那么Retrofit就會幫你設(shè)置一個默認的,具體是怎樣的線程池,我們繼續(xù)看。
private static class Android extends Platform {
...
Executor defaultHttpExecutor() {
return Executors.newCachedThreadPool(new ThreadFactory() {
public Thread newThread(final Runnable r) {
return new Thread(new Runnable() {
public void run() {
Process.setThreadPriority(10);
r.run();
}
}, "Retrofit-Idle");
}
});
}
...
}
直接去Platform的實現(xiàn)類Android,來找到底defaultHttpExecutor是個啥,一看newCachedThreadPool基本就明了了,使用了系統(tǒng)封裝好的緩存線程池,可以再細看一下:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
按照我們上面分析的,核心線程為0個,使用同步隊列,SynchronousQueue是沒有容量的,此處不做詳解,按照目前的邏輯,只要是需要執(zhí)行的task,一旦put了會立馬被take,此處對同步隊列不做詳解,各位可以自行鉆研??梢哉J為一旦有任務(wù)進來,會立馬創(chuàng)建新的線程,如果線程空閑60秒就會被干掉。
因此,理論上來說,這個線程池在極端情況下可以產(chǎn)生Integer.MAX_VALUE個線程,那么當(dāng)這個數(shù)字直線上升的時候,就可以看到華為,魅族,相距的崩掉(出于本身ANR的保護,你可能很難看到3000的魅族崩,甚至其他的上萬限制的手機崩掉)
因此為了避免此類情況的發(fā)生,我們需要給出自定義的線程池方案,你可以選擇有限的最大線程數(shù)配合SynchronousQueue隊列,或者選擇有限的最大線程數(shù)配合有限容量的BlockingQueue其他實現(xiàn)類。
比如
builder.setExecutors(new ThreadPoolExecutor(2, 20, 20L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(64),
new ThreadPoolExecutor.DiscardOldestPolicy()), null);
各位可以自己摸索鉆研到一套最適合自己應(yīng)用場景的線程池配置。
記住,一旦出現(xiàn)了有上限的線程池,必須要設(shè)置溢出策略,上面采用了new ThreadPoolExecutor.DiscardOldestPolicy()方式,該策略會當(dāng)線程池溢出時,拋掉緩存隊列中最先進去的那個,插入新來的。
后記
優(yōu)化線程數(shù),遠遠不止于此。
再比如不同的api域名,通常我們需要創(chuàng)建不同的Retrofit對象,而不同的Retrofit對象,都需要進行初始化,傳入不同的OkhttpClient和相同的OkhttpClient也會有很大的線程數(shù)差距。
重要的還是在平時的編碼過程中,善于觀察,善于測試,善于從崩潰日志中過濾無用信息,定位到線程崩潰點。
最后,為無法理解華為的線程限制抓耳撓腮10分鐘。