国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

大廠Java工程師干掉OOM的套路,不過如此

共 15616字,需瀏覽 32分鐘

 ·

2022-05-13 21:57

關(guān)注Java學(xué)習(xí)之道一起成長(zhǎng),一起學(xué)習(xí)~

作者: 藍(lán)師傅
來源: juejin.cn/post/7074762489736478757

Part1前言

隨著項(xiàng)目不斷壯大,OOM (Out Of Memory)成為奔潰統(tǒng)計(jì)平臺(tái)上的疑難雜癥之一,大部分業(yè)務(wù)開發(fā)人員對(duì)于線上OOM問題一般都是暫不處理,一方面是因?yàn)镺OM問題沒有足夠的log,無法在短期內(nèi)分析解決,另一方面可能是忙于業(yè)務(wù)迭代、身心疲憊,沒有精力去研究OOM的解決方案。

這篇文章將以線上OOM問題作為切入點(diǎn),介紹常見的OOM類型、OOM的原理、大廠OOM優(yōu)化黑科技、以及主流的OOM監(jiān)控方案。

文章較長(zhǎng),請(qǐng)備好小板凳~

歡迎關(guān)注公眾號(hào)"Java學(xué)習(xí)之道",查看更多干貨!

Part2OOM問題分類

很多人對(duì)于OOM的理解就是Java虛擬機(jī)內(nèi)存不足,但通過線上OOM問題分析,OOM可以大致歸為以下3類:

  1. 線程數(shù)太多
  2. 打開太多文件
  3. 內(nèi)存不足

接下來將分別圍繞這三類問題進(jìn)行展開分析~

Part3線程數(shù)太多

3.1 報(bào)錯(cuò)信息

pthread_create (1040KB stack) failed: Out of memory

這個(gè)是典型的創(chuàng)建新線程觸發(fā)的OOM問題

3.2 源碼分析

pthread_create觸發(fā)的OOM異常,源碼(Android 9)位置如下:androidxref.com/9.0.0_r3/xr…[1]

void?Thread::CreateNativeThread(JNIEnv*?env,?jobject?java_peer,?size_t?stack_size,?bool?is_daemon)?{
??...
??pthread_create_result?=?pthread_create(...)
??//創(chuàng)建線程成功
??if?(pthread_create_result?==?0)?{
??????return;
??}
??//創(chuàng)建線程失敗
??...
??{
????std::string?msg(child_jni_env_ext.get()?==?nullptr??
????????StringPrintf("Could?not?allocate?JNI?Env:?%s",?error_msg.c_str())?:
????????StringPrintf("pthread_create?(%s?stack)?failed:?%s",
?????????????????????????????????PrettySize(stack_size).c_str(),?strerror(pthread_create_result)));
????ScopedObjectAccess?soa(env);
????soa.Self()->ThrowOutOfMemoryError(msg.c_str());
??}
}

pthread_create里面會(huì)調(diào)用Linux內(nèi)核創(chuàng)建線程,那什么情況下會(huì)創(chuàng)建線程失敗呢?

查看系統(tǒng)對(duì)每個(gè)進(jìn)程的線程數(shù)限制

cat /proc/sys/kernel/threads-max

不同設(shè)備的threads-max限制是不一樣的,有些廠商的低端機(jī)型threads-max比較小,容易出現(xiàn)此類OOM問題。

查看當(dāng)前進(jìn)程運(yùn)行的線程數(shù)

cat proc/{pid}/status

當(dāng)線程數(shù)超過/proc/sys/kernel/threads-max中規(guī)定的上限時(shí)就會(huì)觸發(fā)OOM。

既然系統(tǒng)對(duì)每個(gè)進(jìn)程的線程數(shù)有限制,那么解決這個(gè)問題的關(guān)鍵就是盡可能降低線程數(shù)的峰值。

3.3 線程優(yōu)化

回看兩年前我寫過一篇文章《面試官:今日頭條啟動(dòng)很快,你覺得可能是做了哪些優(yōu)化?》[2],雖然里面的內(nèi)容有些已經(jīng)過時(shí),不過分析問題的思路還是可以借鑒的,記得當(dāng)時(shí)對(duì)于線程優(yōu)化只是一句話描述,今天這篇文章剛好可以做一個(gè)補(bǔ)充。

歡迎關(guān)注公眾號(hào)"Java學(xué)習(xí)之道",查看更多干貨!

3.3.1 禁用 new Thread

解決線程過多問題,傳統(tǒng)的方案是禁止使用new Thread,統(tǒng)一使用線程池,但是一般很難人為控制, 可以在代碼提交之后觸發(fā)自動(dòng)檢測(cè),有問題則通過郵件通知對(duì)應(yīng)開發(fā)人員。

不過這種方式存在兩個(gè)問題:

  1. 無法解決老代碼的new Thread;
  2. 對(duì)于第三方庫無法控制。

3.3.2 無侵入性的new Thread 優(yōu)化

Java層的Thread只是一個(gè)普通的對(duì)象,只有調(diào)用了start方法,才會(huì)調(diào)用native 層去創(chuàng)建線程,

所以理論上我們可以自定義Thread,重寫start方法,不去啟動(dòng)線程,而是將任務(wù)放到線程池中去執(zhí)行,為了做到無侵入性,需要在編譯期通過字節(jié)碼插樁的方式,將所有new Thread字節(jié)碼都替換成new 自定義Thread。

步驟如下:

1、創(chuàng)建一個(gè)Thread的子類叫ShadowThread吧,重寫start方法,調(diào)用自定義的線程池CustomThreadPool來執(zhí)行任務(wù);

public?class?ShadowThread?extends?Thread?{

????@Override
????public?synchronized?void?start()?{
????????Log.i("ShadowThread",?"start,name="+?getName());
????????CustomThreadPool.THREAD_POOL_EXECUTOR.execute(new?MyRunnable(getName()));
????}

????class?MyRunnable?implements?Runnable?{

????????String?name;
????????public?MyRunnable(String?name){
????????????this.name?=?name;
????????}

????????@Override
????????public?void?run()?{
????????????try?{
????????????????ShadowThread.this.run();
????????????????Log.d("ShadowThread","run?name="+name);
????????????}?catch?(Exception?e)?{
????????????????Log.w("ShadowThread","name="+name+",exception:"+?e.getMessage());
????????????????RuntimeException?exception?=?new?RuntimeException("threadName="+name+",exception:"+?e.getMessage());
????????????????exception.setStackTrace(e.getStackTrace());
????????????????throw?exception;
????????????}
????????}
????}
}

2、在編譯期,hook 所有new Thread字節(jié)碼,全部替換成我們自定義的ShadowThread,這個(gè)難度應(yīng)該不大,按部就班,

我們先確認(rèn)new Threadnew ShadowThread對(duì)應(yīng)字節(jié)碼差異,可以安裝一個(gè)ASM Bytecode Viewer插件,如下所示

通過字節(jié)碼修改,你可以簡(jiǎn)單理解為做如下替換:

3、由于將任務(wù)放到線程池去執(zhí)行,假如線程奔潰了,我們不知道是哪個(gè)線程出問題,所以自定義ShadowThread中的內(nèi)部類MyRunnable 的作用是:在線程出現(xiàn)異常的時(shí)候,將異常捕獲,還原它的名字,重新拋出一個(gè)信息更全的異常。

測(cè)試代碼

private?fun?testThreadCrash()?{
????????Thread?{
????????????val?i?=?9?/?0
????????}.apply?{
????????????name?=?"testThreadCrash"
????????}.start()
????}

開啟一個(gè)線程,然后觸發(fā)奔潰,堆棧信息如下:

可以看到原本的new Thread已經(jīng)被優(yōu)化成了CustomThreadPool線程池調(diào)用,并且奔潰的時(shí)候不用擔(dān)心找不到線程是哪里創(chuàng)建的,會(huì)還原線程名。

當(dāng)然這種方式有一個(gè)小問題,應(yīng)用正常運(yùn)行的情況下,如果你想要收集所有線程信息,那么線程名可能不太準(zhǔn)確,因?yàn)橥ㄟ^new Thread 去創(chuàng)建線程,已經(jīng)被替換成線程池調(diào)用了,獲取到的線程名是線程池中的線程的名字

數(shù)據(jù)對(duì)比

同個(gè)場(chǎng)景簡(jiǎn)單測(cè)試了一下new Thread優(yōu)化前后線程數(shù)峰值對(duì)比:

線程數(shù)峰值(優(yōu)化前)線程數(shù)峰值(優(yōu)化后)降低最大線程數(shù)
33731423

對(duì)于不同App,優(yōu)化效果會(huì)有一些不同,不過可以看到這個(gè)優(yōu)化確實(shí)是有效的。

3.3.3 無侵入的線程池優(yōu)化

隨著項(xiàng)目引入的SDK越來越多,絕大部分SDK內(nèi)部都會(huì)使用自己的線程池做異步操作,

線程池的參數(shù)如果設(shè)置不對(duì),核心線程空閑的時(shí)候沒有釋放,會(huì)使整體的線程數(shù)量處于較高位置。

線程池幾個(gè)參數(shù):
public?ThreadPoolExecutor(int?corePoolSize,
??????????????????????????int?maximumPoolSize,
??????????????????????????long?keepAliveTime,
??????????????????????????TimeUnit?unit,
??????????????????????????BlockingQueue?workQueue,
??????????????????????????ThreadFactory?threadFactory)
?
{
????this(corePoolSize,?maximumPoolSize,?keepAliveTime,?unit,?workQueue,
?????????threadFactory,?defaultHandler);
}
  1. corePoolSize :核心線程數(shù)量。核心線程默認(rèn)情況下即使空閑也不會(huì)釋放,除非設(shè)置allowCoreThreadTimeOut為true。
  2. maximumPoolSize :最大線程數(shù)量。任務(wù)數(shù)量超過核心線程數(shù),就會(huì)將任務(wù)放到隊(duì)列中,隊(duì)列滿了,就會(huì)啟動(dòng)非核心線程執(zhí)行任務(wù),線程數(shù)超過這個(gè)限制就會(huì)走拒絕策略;
  3. keepAliveTime :空閑線程存活時(shí)間
  4. unit:時(shí)間單位
  5. workQueue:隊(duì)列。任務(wù)數(shù)量超過核心線程數(shù),就會(huì)將任務(wù)放到這個(gè)隊(duì)列中,直到隊(duì)列滿,就開啟新線程,執(zhí)行隊(duì)列第一個(gè)任務(wù)。
  6. threadFactory:線程工廠。實(shí)現(xiàn)new Thread方法創(chuàng)建線程
通過線程池參數(shù),我們可以找到優(yōu)化點(diǎn)如下:
  1. 限制空閑線程存活時(shí)間,keepAliveTime 設(shè)置小一點(diǎn),例如1-3s;
  2. 允許核心線程在空閑時(shí)自動(dòng)銷毀
executor.allowCoreThreadTimeOut(true)

如何做呢?為了做到無侵入性,依然采用ASM操作字節(jié)碼,跟new Thread的替換基本同理

在編譯期,通過ASM,做如下幾個(gè)操作:
  1. 將調(diào)用 Executors 類的靜態(tài)方法替換為自定義 ShadowExecutors 的靜態(tài)方法,設(shè)置executor.allowCoreThreadTimeOut(true);
  2. 將調(diào)用 ThreadPoolExecutor 類的構(gòu)造方法替換為自定義 ShadowThreadPoolExecutor 的靜態(tài)方法,設(shè)置executor.allowCoreThreadTimeOut(true)
  3. 可以在 Application 類的 () 中調(diào)用我們自定義的靜態(tài)方法 ShadowAsyncTask.optimizeAsyncTaskExecutor() 來修改 AsyncTask 的線程池參數(shù),調(diào)用executor.allowCoreThreadTimeOut(true);

你可以簡(jiǎn)單理解為做如下替換:

3.4 線程監(jiān)控

假如線程優(yōu)化后還存在創(chuàng)建線程OOM問題,那我們就需要監(jiān)控是否存在線程泄漏的情況。

3.4.1 線程泄漏監(jiān)控

主要監(jiān)控native線程的幾個(gè)生命周期方法:pthread_create、pthread_detach、pthread_join、pthread_exit。

  1. hook 以上幾個(gè)方法,用于記錄線程的生命周期和堆棧,名稱等信息;
  2. 當(dāng)發(fā)現(xiàn)一個(gè)joinable的線程在沒有detach或者join的情況下,執(zhí)行了pthread_exit,則記錄下泄露線程信息;
  3. 在合適的時(shí)機(jī),上報(bào)線程泄露信息。

linux線程中,pthread有兩種狀態(tài)joinable狀態(tài)unjoinable狀態(tài)joinable 狀態(tài)下,當(dāng)線程函數(shù)自己返回退出時(shí)或pthread_exit時(shí) 都不會(huì)釋放線程所占用堆棧和線程描述符。只有當(dāng)你調(diào)用了pthread_join之后 這些資源才會(huì)被釋放,需要main函數(shù)或者其他線程去調(diào)用pthread_join函數(shù)。

3.4.2 線程上報(bào)

當(dāng)監(jiān)控到線程有異常的時(shí)候,我們可以收集線程信息,上報(bào)到后臺(tái)進(jìn)行分析。

收集線程信息代碼如下:

private?fun?dumpThreadIfNeed()?{

????val?threadNames?=?runCatching?{?File("/proc/self/task").listFiles()?}
????????.getOrElse?{
????????????return@getOrElse?emptyArray()
????????}
?????????.map?{
????????????runCatching?{?File(it,?"comm").readText()?}.getOrElse?{?"failed?to?read?$it/comm"?}
????????}
?????????.map?{
????????????if?(it.endsWith("\n"))?it.substring(0,?it.length?-?1)?else?it
????????}
?????????:?emptyList()

????Log.d("TAG",?"dumpThread?=?"?+?threadNames.joinToString(separator?=?","))
}

接下來介紹打開太多文件導(dǎo)致的OOM問題

Part4打開太多文件

4.1 錯(cuò)誤信息

E/art:?ashmem_create_region?failed?for?'indirect?ref?table':?Too?many?open?files
Java.lang.OutOfMemoryError:?Could?not?allocate?JNI?Env

這個(gè)問題跟系統(tǒng)、廠商關(guān)系比較大

4.2 系統(tǒng)限制

Android是基于Linux內(nèi)核,/proc/pid/limits 描述著linux系統(tǒng)對(duì)每個(gè)進(jìn)程的一些資源限制,

如下圖是一臺(tái)Android 6.0的設(shè)備,Max open files的限制是1024

如果沒有root權(quán)限,可以通過ulimit -n命令查看Max open files,結(jié)果是一樣的

ulimit -n

Linux 系統(tǒng)一切皆文件,進(jìn)程每打開一個(gè)文件就會(huì)產(chǎn)生一個(gè)文件描述符fd(記錄在/proc/pid/fd下面)

cd /proc/10654/fd

ls

這些fd文件都是鏈接文件,通過?ls -l可以查看其對(duì)應(yīng)的真實(shí)文件路徑

當(dāng)fd的數(shù)目達(dá)到Max open files規(guī)定的數(shù)目,就會(huì)觸發(fā)Too many open files的奔潰,這種奔潰在低端機(jī)上比較容易復(fù)現(xiàn)。

知道了文件描述符這玩意后,看看怎么優(yōu)化~

歡迎關(guān)注公眾號(hào)"Java學(xué)習(xí)之道",查看更多干貨!

4.2 文件描述符優(yōu)化

對(duì)于打開文件數(shù)太多的問題,盲目?jī)?yōu)化其實(shí)無從下手,總體的方案是監(jiān)控為主。

通過如下代碼可以查看當(dāng)前進(jìn)程的fd信息

private?fun?dumpFd()?{
????val?fdNames?=?runCatching?{?File("/proc/self/fd").listFiles()?}
????????.getOrElse?{
????????????return@getOrElse?emptyArray()
????????}
?????????.map?{?file?->
????????????runCatching?{?Os.readlink(file.path)?}.getOrElse?{?"failed?to?read?link?${file.path}"?}
????????}
?????????:?emptyList()

????Log.d("TAG",?"dumpFd:?size=${fdNames.size},fdNames=$fdNames")

}

4.3 文件描述符監(jiān)控

監(jiān)控策略:當(dāng)fd數(shù)大于1000個(gè),或者fd連續(xù)遞增超過50個(gè),就觸發(fā)fd收集,將fd對(duì)應(yīng)的文件路徑上報(bào)到后臺(tái)。

這里模擬一個(gè)bug,打開一個(gè)文件多次不關(guān)閉,通過dumpFd,可以看到很多重復(fù)的文件名,進(jìn)而大致定位到問題。

當(dāng)懷疑某個(gè)文件有問題之后,我們還需要知道這個(gè)文件在哪創(chuàng)建,是誰創(chuàng)建的,這個(gè)就涉及到IO監(jiān)控~

4.4 IO監(jiān)控

4.4.1 監(jiān)控內(nèi)容

監(jiān)控完整的IO操作,包括open、read、write、close

open :獲取文件名、fd、文件大小、堆棧、線程

read/write :獲取文件類型、讀寫次數(shù)、總大小,使用buffer大小、讀寫總耗時(shí)

close :打開文件總耗時(shí)、最大連續(xù)讀寫時(shí)間

4.4.2 Java監(jiān)控方案:

以Android 6.0 源碼為例,FileInputStream 的調(diào)用鏈如下

java?:?FileInputStream?->?IoBridge.open?->?Libcore.os.open?->??
?BlockGuardOs.open?->?Posix.open

Libcore.java[3]是一個(gè)不錯(cuò)的hook點(diǎn)

package?libcore.io;
public?final?class?Libcore?{
????private?Libcore()?{?}

????public?static?Os?os?=?new?BlockGuardOs(new?Posix());
}

我們可以通過反射獲取到這個(gè)Os變量,它是一個(gè)接口類型,里面定義了open、read、write、close方法,具體實(shí)現(xiàn)在BlockGuardOs[4]里面。

//?反射獲得靜態(tài)變量
Class?clibcore?=?Class.forName("libcore.io.Libcore");
Field?fos?=?clibcore.getDeclaredField("os");

通過動(dòng)態(tài)代理的方式,在它所有IO方法前后加入插樁代碼來統(tǒng)計(jì)IO信息

//?動(dòng)態(tài)代理對(duì)象
Proxy.newProxyInstance(cPosix.getClassLoader(),?getAllInterfaces(cPosix),?this);

beforeInvoke(method,?args,?throwable);
result?=?method.invoke(mPosixOs,?args);
afterInvoke(method,?args,?result);

此方案缺點(diǎn)如下:

  • 性能差,IO調(diào)用頻繁,使用動(dòng)態(tài)代理和Java的字符串操作,導(dǎo)致性能較差,無法達(dá)到線上使用標(biāo)準(zhǔn)
  • 無法監(jiān)控Native代碼,這個(gè)也是比較重要的
  • 兼容性差:需要根據(jù)Android 版本做適配,特別是Android P的非公開API限制

4.4.3 Native監(jiān)控方案

Native Hook方案的核心從 libc.so 中的這幾個(gè)函數(shù)中選定 Hook 的目標(biāo)函數(shù)

int?open(const?char?*pathname,?int?flags,?mode_t?mode);
ssize_t?read(int?fd,?void?*buf,?size_t?size);
ssize_t?write(int?fd,?const?void?*buf,?size_t?size);?write_cuk
int?close(int?fd)
;

我們需要選擇一些有調(diào)用上面幾個(gè)方法的 library,例如選擇libjavacore.so、libopenjdkjvm.so、libopenjdkjvm.so,可以覆蓋到所有的 Java 層的 I/O 調(diào)用。

不同版本的 Android 系統(tǒng)實(shí)現(xiàn)有所不同,在 Android 7.0 之后,我們還需要替換下面這三個(gè)方法。

open64
__read_chk
__write_chk

native hook 框架目前使用比較廣泛的是愛奇藝的xhook[5] ,以及它的改進(jìn)版,字節(jié)跳動(dòng)的bhook[6]。

具體的native IO監(jiān)控代碼,可以參考 Matrix-IOCanary[7],內(nèi)部使用的是xhook框架。

關(guān)于IO涉及到的知識(shí)非常多,后面有時(shí)間可以單獨(dú)整理一篇文章。

接下來看看最后一種OOM類型~

Part5內(nèi)存不足

5.1 堆棧信息

這種是最常見的OOM,Java堆內(nèi)存不足,512M都不夠玩~

發(fā)生此問題的大部分設(shè)備都是Android 7.0,高版本也有,不過相對(duì)較少。

5.2 重溫JVM內(nèi)存結(jié)構(gòu)

JVM在運(yùn)行時(shí),將內(nèi)存劃分為以下5個(gè)部分

  1. 方法區(qū):存放靜態(tài)變量、常量、即時(shí)編譯代碼;
  2. 程序計(jì)數(shù)器:線程私有,記錄當(dāng)前執(zhí)行的代碼行數(shù),方便在cpu切換到其它線程再回來的時(shí)候能夠不迷路;
  3. Java虛擬機(jī)棧:線程私有,一個(gè)Java方法開始和結(jié)束,對(duì)應(yīng)一個(gè)棧幀的入棧和出棧,棧幀里面有局部變量表、操作數(shù)棧、返回地址、符號(hào)引用等信息;
  4. 本地方法棧:線程私有,跟Java虛擬機(jī)棧的區(qū)別在于 這個(gè)是針對(duì)native方法;
  5. 堆:絕大部分對(duì)象創(chuàng)建都在堆分配內(nèi)存

內(nèi)存不足導(dǎo)致的OOM,一般都是由于Java堆內(nèi)存不足,絕大部分對(duì)象都是在堆中分配內(nèi)存,除此之外,大數(shù)組、以及Android3.0-7.0的Bitmap像素?cái)?shù)據(jù),都是存放在堆中。

Java堆內(nèi)存不足導(dǎo)致的OOM問題,線上難以復(fù)現(xiàn),往往比較難定位到問題,絕大部分設(shè)備都是8.0以下的,主要也是由于Android 3.0-7.0 Bitmap像素內(nèi)存是存放在堆中 導(dǎo)致的。

基于這個(gè)結(jié)論,關(guān)于Java堆內(nèi)存不足導(dǎo)致的OOM問題,優(yōu)化方案主要是圖片加載優(yōu)化、內(nèi)存泄漏監(jiān)控 。

5.3 圖片加載優(yōu)化

5.3.1 常規(guī)的圖片優(yōu)化方式

常規(guī)的圖片加載優(yōu)化,依然可以參考兩年前的一篇文章《面試官:簡(jiǎn)歷上最好不要寫Glide,不是問源碼那么簡(jiǎn)單》[8], 文章核心內(nèi)容大概如下:

  1. 分析了主流圖片庫Glide和Fresco的優(yōu)缺點(diǎn),以及使用場(chǎng)景;
  2. 分析了設(shè)計(jì)一個(gè)圖片加載框架需要考慮的問題;
  3. 防止圖片占用內(nèi)存過多導(dǎo)致OOM的三個(gè)方式:軟引用、onLowMemory、Bitmap 像素存儲(chǔ)位置

這篇文章現(xiàn)在來看還是有點(diǎn)意義的,其中的原理部分還沒過時(shí),不過技術(shù)更新迭代,常規(guī)的優(yōu)化方式已經(jīng)不太夠了,長(zhǎng)遠(yuǎn)考慮,可以做圖片自動(dòng)壓縮、大圖自動(dòng)檢測(cè)和告警

5.3.2 無侵入性自動(dòng)壓縮圖片

針對(duì)圖片資源,設(shè)計(jì)師往往會(huì)追求高清效果,忽略圖片大小,一般的做法是拿到圖后手動(dòng)壓縮一下,這種手動(dòng)的操作完全看個(gè)人修養(yǎng)。

無侵入性自動(dòng)壓縮圖片,主流的方案是利用Gradle 的Task原理,在編譯過程中,mergeResourcesTask 這個(gè)任務(wù)是將所以aar、module的資源進(jìn)行合并,我們可以在mergeResourcesTask 之后可以拿到所有資源文件,具體做法:

  1. mergeResourcesTask這個(gè)任務(wù)后面,增加一個(gè)圖片處理的Task,拿到所有資源文件;
  2. 拿到所有資源文件后,判斷如果是圖片文件,則通過壓縮工具進(jìn)行壓縮,壓縮后如果圖片有變小,就將壓縮過的圖片替換掉原圖。

可以簡(jiǎn)單理解如下:

具體代碼可以參考 McImage[9] 這個(gè)庫。

5.4 大圖監(jiān)控

5.3.2 自動(dòng)壓縮圖片只是針對(duì)本地資源,而對(duì)于網(wǎng)絡(luò)圖片,如果加載的時(shí)候沒有壓縮,那么內(nèi)存占用會(huì)比較大,這種情況就需要監(jiān)控了。

5.4.1 從圖片框架側(cè)監(jiān)控

很多App內(nèi)部可能使用了多個(gè)圖片庫,例如Glide、Picasso、Fresco、ImageLoader、Coil,如果想監(jiān)控某個(gè)圖片框架, 那么我們需要熟讀源碼,找到hook點(diǎn)。

對(duì)于Glide,可以通過hook SingleRequest,它里面有個(gè)requestListeners,我們可以注冊(cè)一個(gè)自己的監(jiān)聽,圖片加載完做一個(gè)大圖檢測(cè)。

其它圖片框架,同理也是先找到hook點(diǎn),然后進(jìn)行類似的hook操作就可以,代碼可以參考:dokit-BigImgClassTransformer[10]

5.4.2 從ImageView側(cè)監(jiān)控

5.4.1 是從圖片加載框架側(cè)監(jiān)控大圖,假如項(xiàng)目中使用到的圖片加載框架太多,有些第三方SDK內(nèi)部可能自己搞了圖片加載,

這種情況下我們可以從ImageView控件側(cè)做監(jiān)控,監(jiān)聽setImageDrawable等方法,計(jì)算圖片大小如果大于控件本身大小,debug包可以彈窗提示需要修改。

方案如下:

  1. 自定義ImageView,重寫setImageDrawable、setImageBitmap、setImageResource、setBackground、setBackgroundResource這幾個(gè)方法,在這些方法里面,檢測(cè)Drawable大小;
  2. 編譯期,修改字節(jié)碼,將所有ImageView的創(chuàng)建都替換成自定義的ImageView;
  3. 為了不影響主線程,可以使用IdleHandler,在主線程空閑的時(shí)候再檢測(cè);

最終是希望當(dāng)檢測(cè)到大圖的時(shí)候,debug環(huán)境能夠彈窗提示開發(fā)進(jìn)行修改,release環(huán)境可以上報(bào)后臺(tái)。

debug如下效果:

當(dāng)然這種方案有個(gè)缺點(diǎn):不能獲取到圖片url。

圖片優(yōu)化告一段落,接下來看看內(nèi)存泄漏~

5.5 內(nèi)存泄漏監(jiān)控演進(jìn)

LeakCanary

關(guān)于內(nèi)存泄漏,大家可能都知道LeakCanary[11],只要添加一個(gè)依賴

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'

就能實(shí)現(xiàn)自動(dòng)檢測(cè)和分析內(nèi)存泄漏,并發(fā)出一個(gè)通知顯示內(nèi)存泄漏詳情信息。

LeakCanary只能在debug環(huán)境使用,因?yàn)樗窃诋?dāng)前進(jìn)程dump內(nèi)存快照,Debug.dumpHprofData(path);會(huì)凍結(jié)當(dāng)前進(jìn)程一段時(shí)間,整個(gè) APP 會(huì)卡死約5~15s,低端機(jī)上可能要幾十秒的時(shí)間。

ResourceCanary

微信對(duì)LeakCanary做了一些改造,將檢測(cè)和分析分離,客戶端只負(fù)責(zé)檢測(cè)和dump內(nèi)存鏡像文件,文件裁剪后上報(bào)到服務(wù)端進(jìn)行分析。

具體可以看這篇文章Matrix ResourceCanary -- Activity 泄漏及Bitmap冗余檢測(cè)[12]

KOOM

不管是LeakCanary 還是 ResourceCanary,他們都只能在線下使用,而線上內(nèi)存泄漏監(jiān)控方案,目前KOOM[13]的方案比較完善,下面我將基于KOOM分析線上內(nèi)存泄漏監(jiān)控方案的核心流程。

5.6 線上內(nèi)存泄漏監(jiān)控方案

基于KOOM源碼分析

5.6.1 檢測(cè)時(shí)機(jī)

  1. 間隔5s檢測(cè)一次
  2. 觸發(fā)內(nèi)存鏡像采集的條件:
  • 當(dāng)內(nèi)存使用率達(dá)到80%以上
//->OOMMonitorConfig
private?val?DEFAULT_HEAP_THRESHOLD?by?lazy?{
??val?maxMem?=?SizeUnit.BYTE.toMB(Runtime.getRuntime().maxMemory())
??when?{
????maxMem?>=?512?-?10?->?0.8f
????maxMem?>=?256?-?10?->?0.85f
????else?->?0.9f
??}
}
  • 兩次檢測(cè)時(shí)間內(nèi)(例如5s內(nèi)),內(nèi)存使用率增加5%

5.6.2 內(nèi)存鏡像采集

我們知道LeakCanary檢測(cè)內(nèi)存泄漏,不能用于線上,是因?yàn)樗黡ump內(nèi)存鏡像是在當(dāng)前進(jìn)程進(jìn)行操作,會(huì)凍結(jié)App一段時(shí)間。

所以,作為線上OOM監(jiān)控,dump內(nèi)存鏡像需要單獨(dú)開一個(gè)進(jìn)程。

整體的策略是:

虛擬機(jī)supend->fork虛擬機(jī)進(jìn)程->虛擬機(jī)resume->dump內(nèi)存鏡像的策略。

dump內(nèi)存鏡像的源碼如下:

//->ForkJvmHeapDumper
public?boolean?dump(String?path)?{
??...

??boolean?dumpRes?=?false;
??try?{
????//1、通過fork函數(shù)創(chuàng)建子進(jìn)程,會(huì)返回兩次,通過pid判斷是父進(jìn)程還是子進(jìn)程
????int?pid?=?suspendAndFork();

????MonitorLog.i(TAG,?"suspendAndFork,pid="+pid);
????if?(pid?==?0)?{
??????//2、子進(jìn)程返回,dump內(nèi)存操作,dump內(nèi)存完成,退出子進(jìn)程
??????Debug.dumpHprofData(path);
??????exitProcess();
????}?else?if?(pid?>?0)?{
??????//?3、父進(jìn)程返回,恢復(fù)虛擬機(jī),將子進(jìn)程的pid傳過去,阻塞等待子進(jìn)程結(jié)束
??????dumpRes?=?resumeAndWait(pid);
??????MonitorLog.i(TAG,?"notify?from?pid?"?+?pid);
????}
??}
??return?dumpRes;
}

注釋1:父進(jìn)程調(diào)用native方法掛起虛擬機(jī),并且創(chuàng)建子進(jìn)程;注釋2:子進(jìn)程創(chuàng)建成功,執(zhí)行Debug.dumpHprofData,執(zhí)行完后退出子進(jìn)程;注釋3:得知子進(jìn)程創(chuàng)建成功后,父進(jìn)程恢復(fù)虛擬機(jī),解除凍結(jié),并且當(dāng)前線程等待子進(jìn)程結(jié)束。

注釋1源碼如下:

//?->native_bridge.cpp

pid_t?HprofDump::SuspendAndFork()?{
??//1、暫停VM,不同Android版本兼容
??if?(android_api_?????suspend_vm_fnc_();
??}
??...

??//2,fork子進(jìn)程,通過返回值可以判斷是主進(jìn)程還是子進(jìn)程
??pid_t?pid?=?fork();
??if?(pid?==?0)?{
????//?Set?timeout?for?child?process
????alarm(60);
????prctl(PR_SET_NAME,?"forked-dump-process");
??}
??return?pid;
}

注釋3源碼如下:

//->hprof_dump.cpp

bool?HprofDump::ResumeAndWait(pid_t?pid)?{
??//1、恢復(fù)虛擬機(jī),兼容不同Android版本
??if?(android_api_?????resume_vm_fnc_();
??}
??...
??int?status;
??for?(;;)?{
????//2、waitpid,等待子進(jìn)程結(jié)束
????if?(waitpid(pid,?&status,?0)?!=?-1?||?errno?!=?EINTR)?{
??????//進(jìn)程異常退出
??????if?(!WIFEXITED(status))?{
????????ALOGE("Child?process?%d?exited?with?status?%d,?terminated?by?signal?%d",
??????????????pid,?WEXITSTATUS(status),?WTERMSIG(status));
????????return?false;
??????}
??????return?true;
????}
????return?false;
??}
}

這里主要是利用Linux的waitpid函數(shù),主進(jìn)程可以等待子進(jìn)程dump結(jié)束,然后再返回執(zhí)行內(nèi)存鏡像文件分析操作。

5.6.3 內(nèi)存鏡像分析

前面一步已經(jīng)通過Debug.dumpHprofData(path)拿到內(nèi)存鏡像文件,接下來就開啟一個(gè)后臺(tái)服務(wù)來處理

//->HeapAnalysisService
?
??override?fun?onHandleIntent(intent:?Intent?)?{
????...
????kotlin.runCatching?{
??????//1、通過shark將hprof文件轉(zhuǎn)換成HeapGraph對(duì)象
??????buildIndex(hprofFile)
????}
????...
????//2、將設(shè)備信息封裝成json
????buildJson(intent)

????kotlin.runCatching?{
??????//3、過濾泄漏對(duì)象,有幾個(gè)規(guī)制
??????filterLeakingObjects()
????}
????...
????kotlin.runCatching?{
??????//?4、gcRoot是否可達(dá),判斷內(nèi)存泄漏
??????findPathsToGcRoot()
????}
????...

????//5、泄漏信息填充到j(luò)son中,然后結(jié)束了
????fillJsonFile(jsonFile)


????//通知主進(jìn)程內(nèi)存泄漏分析成功
????resultReceiver?.send(AnalysisReceiver.RESULT_CODE_OK,?null)

????//這個(gè)服務(wù)是在單獨(dú)進(jìn)程,分析完就退出
????System.exit(0);
??}

內(nèi)存鏡像分析的流程如下:

  1. 通過shark這個(gè)開源庫將hprof文件轉(zhuǎn)換成HeapGraph對(duì)象
  2. 收集設(shè)備信息,封裝成json,現(xiàn)場(chǎng)信息很重要
  3. filterLeakingObjects:過濾出泄漏的對(duì)象,有一些規(guī)制,例如已經(jīng)destroyed和finished的activity、fragment manager為空的fragment、已經(jīng)destroyed的window等。
  4. findPathsToGcRoot:內(nèi)存泄漏的對(duì)象,查找其到GcRoot的路徑,通過這一步就可以揪出內(nèi)存泄漏的原因
  5. fillJsonFile:格式化輸出內(nèi)存泄漏信息

小結(jié)

線上Java內(nèi)存泄漏監(jiān)控方案分析,這里小結(jié)一下:

  1. 掛起當(dāng)前進(jìn)程,然后通過fork創(chuàng)建子進(jìn)程;
  2. fork會(huì)返回兩次,一次是子進(jìn)程,一次是父進(jìn)程,通過返回的pid可以判斷是子進(jìn)程還是父進(jìn)程;
  3. 如果是父進(jìn)程返回,則通過resumeAndWait恢復(fù)進(jìn)程,然后當(dāng)前線程阻塞等待子進(jìn)程結(jié)束;
  4. 如果子進(jìn)程返回,通過Debug.dumpHprofData(path)讀取內(nèi)存鏡像信息,這個(gè)會(huì)比較耗時(shí),執(zhí)行結(jié)束就退出子進(jìn)程;
  5. 子進(jìn)程退出,父進(jìn)程的resumeAndWait就會(huì)返回,這時(shí)候就可以開啟一個(gè)服務(wù),后臺(tái)分析內(nèi)存泄漏情況,這塊跟LeakCanary的分析內(nèi)存泄漏原理基本差不多。

不畫圖了,結(jié)合源碼看應(yīng)該可以理解。

5.7 native內(nèi)存泄漏監(jiān)控

對(duì)于Java內(nèi)存泄漏監(jiān)控,線下我們可以使用LeakCanary、線上可以使用KOOM,而對(duì)于native內(nèi)存泄漏應(yīng)該如何監(jiān)控呢?

方案如下:

首先要了解native層 申請(qǐng)內(nèi)存的函數(shù):malloc、realloc、calloc、memalign、posix_memalign釋放內(nèi)存的函數(shù):free

  1. hook申請(qǐng)內(nèi)存和釋放內(nèi)存的函數(shù)

分配內(nèi)存的時(shí)候,收集堆棧、內(nèi)存大小、地址、線程等信息,存放到map中,在釋放內(nèi)存的時(shí)候從map中移除。

那怎么判斷native內(nèi)存泄漏呢?

  • 周期性的使用 mark-and-sweep 分析整個(gè)進(jìn)程 Native Heap,獲取不可達(dá)的內(nèi)存塊信息「地址、大小」
  • 獲取到不可達(dá)的內(nèi)存塊的地址后,可以從我們的Map中獲取其堆棧、內(nèi)存大小、地址、線程等信息。

具體實(shí)現(xiàn)可以參考:koom-native-leak[14]

Part6總結(jié)

本文從線上OOM問題入手,介紹了OOM原理, 以及OOM優(yōu)化方案和監(jiān)控方案,基本上都是大廠開源出來的比較成熟的方案:

  1. 對(duì)于pthread_create OOM問題,介紹了無侵入性的new Thread優(yōu)化、無侵入性的線程池優(yōu)化、以及線程泄漏監(jiān)控;
  2. 對(duì)于文件描述符過多問題,介紹了原理以及文件描述符監(jiān)控方案、IO監(jiān)控方案;
  3. 對(duì)于Java內(nèi)存不足導(dǎo)致的OOM、介紹了無侵入性圖片自動(dòng)壓縮方案、兩種無侵入性的大圖監(jiān)控方案、Java內(nèi)存泄漏監(jiān)控的線下方案和線上方案、以及native內(nèi)存泄漏監(jiān)控方案。

大廠對(duì)外開源的技術(shù)非常多,但不一定最優(yōu),我們?cè)趯W(xué)習(xí)過程中可以多加思考, 例如線程優(yōu)化,booster 對(duì)于new Thread的優(yōu)化只是設(shè)置了線程名,有助于分析問題,而經(jīng)過我的猜想和驗(yàn)證,通過字節(jié)碼插樁,將new Thread無侵入性替換成線程池調(diào)用,才是真正意義上的線程優(yōu)化。

參考資料

[1]

http://androidxref.com/9.0.0_r3/xref/art/runtime/thread.cc

[2]

https://juejin.cn/post/6844903958113157128

[3]

http://androidxref.com/6.0.1_r10/xref/libcore/luni/src/main/java/libcore/io/Libcore.java

[4]

http://androidxref.com/6.0.1_r10/xref/libcore/luni/src/main/java/libcore/io/BlockGuardOs.java

[5]

https://github.com/iqiyi/xHook/blob/master/README.zh-CN.md

[6]

https://github.com/bytedance/bhook/blob/main/doc/overview.zh-CN.md

[7]

https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-io-canary/src/main/cpp/io_canary_jni.cc

[8]

https://juejin.cn/post/6844903986412126216

[9]

https://github.com/smallSohoSolo/McImage

[10]

https://github.com/didi/DoraemonKit/blob/master/Android/buildSrc/src/main/kotlin/com/didichuxing/doraemonkit/plugin/classtransformer/BigImgClassTransformer.kt

[11]

https://github.com/square/leakcanary/

[12]

https://mp.weixin.qq.com/s/XL55txToSCJXM8ErwrUGMw

[13]

https://github.com/KwaiAppTeam/KOOM/blob/master/README.zh-CN.md

[14]

https://github.com/KwaiAppTeam/KOOM/blob/master/koom-native-leak/README.zh-CN.md

-- END?--

-??| 更多精彩文章 -



← 左右滑動(dòng)與Java學(xué)習(xí)之道互動(dòng)交流 →

加我微信,交個(gè)朋友
長(zhǎng)按/掃碼添加↑↑↑

瀏覽 32
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 91在线无码精品秘国产色多多| 天天艹天天| 日韩AV在线直播| 日日干天天干| 91成人一区二区三区| 京东一热本色道久久爱| 中文字幕第11页| 亚洲中文无码av| 国产精品黑人ThePorn| 日本色网址| 欧一美一婬一伦一区| 九色国产视频| 91人妻人人澡人人爽精品| 亚洲成人无码高清| 美女视频黄a视频全免费不卡| 欧美在线天堂| 秋霞一区| 亚洲国产精品成人久久蜜臀| 欧美射精视频| 日韩免费成人视频| 日日夜夜天天综合| 久久久18禁一区二区三区精品| 青青av| 天天操夜夜操狠狠操| 黄网站在线免费| 91麻豆电影| 黄色免费在线观看网站| 91探花足浴店按摩店| 日韩一级欧美一级| 欧美一级aa| 竹菊av一区二区三区四区五区| 一级片黄色电影| 亚洲福利女神成人福利| 特级丰满少妇一级AAAA爱毛片| 日韩视频一区二区| 日本不卡一区二区三区| 久热精品在线观看| 大香蕉伊人在线观看视频| 国产日韩欧美一区二区| 一级Aa视频免费看| 色五月AV| 夜夜撸天天干| 一级特黄色片| 97精品人妻一区二区三区香蕉| 午夜激情国产| 久久久在线视频| 亚洲AV无码精品岛国| 99九九99九九九99九他書對| 91丝袜足交| 色图在线观看| 日韩三级AV在线观看| 欧美黄色一级| 六月婷婷五月丁香| 亚洲av电影在线观看| 国产性爱AV| 西西444WWW大胆无视频软件亮点 | 成人电影一区二区| 激情深爱五月| 欧美性性生交XXXXX无码| 91香蕉网站| 久久天堂影院| 日韩黄色A片| 丁香激情视频| 免费福利在线视频| www久草| 噜噜噜网| 人妻人人妻| 白丝久久| 国产精品亚洲一区| 一区二区三区四区日韩| 黄色片在线视频| 在线观看无码AV| 自拍偷拍一区二区三区| 日本午夜影院| 麻豆天美蜜桃91| 久久久午夜| 久久国产高清视频| 久久福利电影| 丁香AV| 亚洲中文中出| 成人黄A片免费| 国产一区二区三区免费观看| 动漫啪啪视频| 四川BBB嫩BBBB爽BBBB| 操美女视频网站| 亚洲A片一区二区三区电影网 | 国产一级婬片A片AAA樱花| 性爱av在线| 青青超碰| 成人福利网站| 久久久久成人片免费观看蜜芽| 尤物无码| 无码人妻一区二区三区精品不付款| 男女AV| 午夜av在线| 国产一级黄色| 国产av高清| 懂色AV| 黄色影视不卡| 国产人与禽zoz0性伦| 成人做爱免费看| 成人色色视频| 中国老少配BBwBBwBBW| 国产美女被爽到高潮免费A片软件| 五月天婷婷在线视频| 秋霞欧美在线| 亚洲大片在线观看| 狠狠欧美| 苍井空一区二区三区| 看看AV| 国产精品网站在线观看| 亚洲成人在线免费观看| 免费一区二区三区四区| 一区二区三区四区五区六区高清无吗视频 | 免费视频一二区| 台湾无码精品| 51妺嘿嘿午夜福利视频| 日本一区不卡| 男女视频91| 俺来也俺也啪WWW色| 蜜桃传媒一区二区亚洲| 免费日逼| 国产一级黄色| 精品久久免费| 91精品国产乱码| 久久久精品在线| 色婷婷一二三精品A片| 五月婷在线观看| 影音av资源| 亚洲AV无码| 午夜免费视频1000| 香蕉成人A片视频| 国产精品久久久| 国产成人精品无码片区在线观91 | 成人aV无码精品国产一区二区 | 欧美伦妇AAAAAA片| 亚欧久久| 免费一级无码婬片A片APP直播 | 欧美喷水视频| av影音先锋在线| 69视频在线播放| 日韩一级片在线播放| 激情视频在线免费观看| 91无码电影| 一区二区三区免费观看| 久操免费在线| 夜夜撸夜夜操| 欧美日韩国产成人电影| 综合久久中文字幕| 四虎高清无码| 大香伊人| 97在线鲁碰免费视频| AV在线小说| 日韩中文字幕在线高清| 蜜臀久久久99久久久久久久| 97精品国产97久久久久久免费 | 欧美成人视频。| 国产高清精品在线| 懂色av蜜臀av粉嫩av分| 欧美AAA在线观看| 西西人体444rt高清大胆模特| 欧美性爱一区二区三区| 亚洲另类视频| 日韩二| 尻屄视频网站| 欧美日韩小电影| 五月丁香亭亭| 777久久| 亚洲欧美日韩在线| 欧美爱爱试看| 日本黄色三级| 国产免费高清| 国产免费AV在线| 色婷婷视频在线观看| 九九九九精品视频| 日本中文字幕在线观看视频| 色婷婷影音| 五月天国产精品| 一级日韩| 在线播放日韩| 超碰九色| 骚妇大战黑人15P| 亚洲第一页在线| 黄页网站在线观看| 人妻无码在线观看| 91搞| 91人人在线| 人人爱人人插| 91人妻无码一区二区久久| 国产91丝袜在线播放| 亚洲无码人妻视频| 色五月婷婷综合| 一区二区高清| 丁香花在线小说免费阅读| 丁香五月综合啪啪| 免费AV在线播放| 蜜桃Av噜噜一区二区| 殴殴美日韩在线| 国产精品一区二区AV日韩在线| 日本色网址| 日韩国无码| 青草福利| 狠狠干老司机| 国产成人av在线观看| 亚洲一级黄色视频| 中文字幕在线字幕中文乱码区别| 北条麻妃性爱视频| 欧美日韩一区二区三区视频| av在线观看中文字幕| 四虎欧美| 欧美18禁网站| 欧美一区二区在线观看| 无码毛片在线观看| 国产亚洲欧洲| 牛牛Av| 中文亚洲精品字幕电影| 狠狠操电影| 在线观看你懂得| 日韩三级片av| 日本一级黄色| 操逼网站在线观看| 99久久黄色| 内射视频网| 免费AV在线播放| www.日韩精品| 午夜成人精品一区二区三区| 日韩爱爱爱| 日本黄色电影网址| 亚洲成人视频在线播放| www.日本黄色视频| 日本人妻中文字幕| 星空AV| 北条麻妃在线视频聊天| 五月天婷婷色| 人人操天天干| 黄页网址在线观看| 国产激情久久| 秘蜜桃色一区二区三区在线观看| 国产欧美综合在线| 天天干天天干| 91香蕉视频免费| 久久久久久久久免费看无码| 久久香蕉人| 国产人体视频| 亚洲一本| 色哟哟一区二区三区四区| 无码123区| 亚洲秘无码一区二区三区观看| 51嘿嘿嘿国产精品伦理| 18禁黄色免费网站| 欧美色性乐汇操日本娘们| 国产亚洲精品午夜福利巨大软件| 刘玥91精一区二区三区| 一本视频| 91三级电影| 国产强伦轩免费视频在线| 日韩精品一| 麻豆自拍偷拍| 国产精品免费av在线| BBw日本熟妇BBwHD| h片免费在线观看| 日韩中文字幕成人| 自拍偷拍av| 97人人艹| 三级成人免费| 成人AV毛片| 五月天AV网站| 五月天综合| 中文字幕国产| 天天精品视频| 婷婷啪啪| 特级西西444www大胆高清图片 | 久久久久久久香蕉视频| 五月丁香视频在线观看| 国产美女高潮| 日日夜夜综合| 2025AV天堂| 天堂v在线观看| 亚洲AV无码永久精品| 亚洲视频在线观看免费| 成人69AV| 黄色成人在线视频| 影音先锋久久久久AV综合网成人| 激情视频在线免费观看| 天天亚洲| 高潮AV在线观看| 超碰乱交av在线| 嫩小槡BBBB槡BBBB槡免费-百度 | 26∪u∪成人网站| 黄色av免费看| 玖玖99视频| 久久夜色视频网| 亚洲欧美视频一区| 欧美日韩国产在线观看| 亚洲中文字幕日韩| 色欲99| 成年女人毛片| 国产精品国产成人国产三级| 亚洲AV永久无码国产精品久久| 国产一级片免费看| 日韩小电影在线观看| 久操视频网站| 欧美成人网站视频| 日韩精品一区二区在线观看| 亚洲福利影院| 色噜噜狠狠一区二区三区牛牛影视| 91久久婷婷国产麻豆精品电影.co| 青青草原在线视频免费观看| 无码在线免费观看视频| 广州媚黑妇系列视频在线| 成人在线中文字幕| 黄色爱爱视频| 蜜桃91视频| 色色色五月婷婷| 成人黄片视频| 高清无码高潮| 日本精品在线| 久久久久久无码精品亚洲日韩麻豆 | 一区在线视频| 免费69视频看片| 成人网站在线观看免费| 摸BBB搡BBB搡BBBB| 欧美一区二区三区在线| 大学生一级特黄大片| 免费看操逼| 亚洲男女免费视频| 亚洲精品乱码久久久久| 中文字幕色站| 99re在线| 国产精品污www在线观看| 欧美国产日韩另类| 综合久久中文字幕| 欧美狼友| 久久久久久久久久久成人| 婷婷视频网站| 日本成人三级片| 国产一区在线播放| 精品一区二区三区免费毛片| 国产wwwww| 久热精品在线观看| 91成人A片| 亚洲AV成人无码精在线| 18啪啪网站| 欧美不卡视频| 91鲁| 逼逼AV网站-日韩电影| 成人A片免费视频| 久久精品视频久久| 青青操网| 高清无码网| 91色伦| 日韩在线中文字幕| 国产九九| 欧美三级理论片| 久久久国产探花视频| 麻豆md0049免费| 色色一级| 亚洲色图一区二区三区| 福利视频免费观看| 欧美日在线| 中文字幕巨肉乱码中文乱码| 欧美精品一区二区三区四区| 99热在线只有精品| 国产老熟女高潮毛片A片仙踪林| 一区二区国产精品| 日韩免费网| 黄色片a| 91久久久久久久18| 久久777| 天天肏天天肏| 大香蕉伊人网在线| 大鸡巴久久久| 探花一区二区| 在线看片a| 免费视频久久| 欧美视频A| 蜜桃av在线播放| 3D动漫操逼视频| 国产拍拍拍| 国产无遮挡又黄又爽又色| 午夜福利影院在线| 久久免费视频精品| 激情小说在线观看| 操逼在线观看| 日韩一片| 国产高清黑人| 欧美日韩高清在线| 伊人青青操| 亚洲秘AV无码一区二区qq群| 日批视频在线观看| 特级西西444www| 成人黄色免费网站| 亚洲黄在线观看| 五月婷婷色欲| 天天干一干| 伊人成人在线视频观看| 三级丁香在线| 久久久久久高清毛片一级| 九色蝌蚪9l视频蝌蚪9l视频成人熟妇| 人人爱人人草| 人人草在线观看| 翔田千里在线一区二区三区| 五月婷婷六月激情| 69视频在线播放| 操逼视频一区| 人妻少妇一区二区三区| 色欧美视频| 久久68| 无码一区二区视频| 免费黄色a片| 国产AV高清| 成人a视频| 久久久91人妻无码精品蜜桃ID| 日韩一级片在线观看| 国产高清无码18| 综合自拍偷拍| 成人午夜福利| 久久国产激情| 二级黄色视频| 男女操逼网站| 伊人国产女| 久草视频在线免费播放| 波多野结衣无码视频| 国产在线无码观看| 日韩精品视频免费| 国产AV一区二区三区| 操日本美女| 午夜天堂在线| 大香蕉精品欧美色综合2025| 影音AV| 开心激情婷婷| r四虎18| 激情一区二区三区| 欧美日韩免费在线| 91一区在线观看| 内射精品| 欧美Aⅴ| 国产伦精品一级A片视频夜夜| 无码三级| 男人天堂2024| 日本天堂在线视频| 成人A片在线播放| 天天日天天插| 五月天婷婷色色| 亚洲成人无码高清| 天堂无码视频| 日韩免费黄色电影| 亚洲激情自拍| 91亚色视频| 安徽扫搡BBBB揉BBBB| 国产日韩欧美一区| 亚洲深夜福利| 大吊无码| 99热免费在线观看| 91人妻人人澡人人爽精品| 一级片电影网站| 五十路在线| 久久区| 精品黄色电影| 日韩性网| 一区二区无码视频| 奇米色五月| 九一亚洲精品| 西西444WWW无码视频软件| 成人视频在线观看黄色18| 91.xxxx| 国产欧美激情| 爱爱视频免费看| 婷婷无码在线| 天天色操| 亚洲一级免费在线观看| 免费黄色毛片| 自拍视频在线观看| 青青草原在线视频免费观看| 中文字幕五月久久婷婷| 欧美操逼逼| 美女91视频网站| 欧美福利在线观看| 特级西西444www高清大胆免费看| 大雞巴疯狂浓精合集| 天天撸天天射| 日韩人妻精品中文字幕专区不卡| 99精品9| 五月天青青草超碰免费公开在线观看 | 青青草无码成人天堂免费| 欧美一区二区三区成人片在线| www99精品| 欧美A级视频在线观看| 操毛| 国产在线视频一区二区| 自慰影院| 国产特黄视频| 成人国产精品秘在线看| 午夜黄色福利| 北条麻妃人妻中文字幕91影视| 国产自慰一区| 无码日逼视频| 在线观看免费黄色视频| 怡红院视频| 黄色在线免费观看网站| 久久久久久少妇| 丁香婷婷久久久综合精品国产| 一区二区高清| 豆花视频logo进入官网| 成人免费视频国产在线观看 | AV性爱在线| XXXX国产| 国产污视频| 伊人成人在线| 国产女人在线观看| 搡中国东北老女人视频| 亚洲午夜电影| 日韩毛片在线免费观看| 一区二区Av| 日韩欧美在线播放| 成人视频在线观看免费| 人人操人人摸人人看| 刘玥一级婬片A片AAA| 国产精品无码成人AV电影| 久久精品大屁股| 伊人操| 制服乱伦| 亚洲性爱小说| 男女拍拍拍拍| 老熟女--91XX| 亚州精品国产精品乱码不99勇敢| 亚洲一级在线| 四虎黄色网址| 欧美XXX视频| www久久99| 精品AAA| 国产精品成人69| 欧美色性乐汇操日本娘们| 天天射日日干| 蜜桃av在线播放| 日韩中文无| 大香蕉在线网站| 精品久久无码| 日本色五月| 91大神免费在线观看| 国产亚洲色婷婷久久99精品91| a网站在线观看| 先锋av资源网| 欧美日韩中文字幕在线视频| 18禁网站免费观看| 688AV秘无码一区二区| 日本草久| 操逼视频免费观看| 欧美日韩精品一区二区三区钱| 亚洲AV无码精品| 国产无遮挡又黄又爽| 中文字幕免费AV| 中字AV| 成人片网站在线观看| 激情aaa| 日韩一级视频| 欧美日韩毛| 蜜桃Av噜噜一区二区| 久99| 91av无码| www.91com| 天堂在线9| 啪啪网站免费看| 亚洲视频99| 青草久在线| 香蕉黄色三级片| 亚洲秘无码一区二区三区观看| 色五月综合| 天天操人妻| 亚洲国产成人91PORN| 麻豆激情视频| 竹菊传媒一区二区三区| 国产在线拍揄自揄拍无码男男| 五月天黄色电影网站| 人人操人人超碰| 日韩AAA在线| 亚洲欧美色图| 婷婷综合| 精品国产久久久| 91AV在线看| 十八禁在线播放| 欧美丰满人妻| 好男人WWW一区二区三区| 亚洲男人的天堂网| 青青草逼视频| 屁屁影院CCYYCOM国产| 天堂无码高清| 男人操女人视频网站| 熟女人妻一区二区三区免费看| 亚洲AV秘一区二区色盗战流出| 人妻电影亚洲av| 97国产在线| 日本a片| 91成人电影在线观看| 午夜av影院| 俺来也俺就去www色情网| 国产一a毛一a毛A免费| 久久草大香蕉| 波多野结衣无码高清视频| 成人A片免费观看| 亚洲电影在线| 久久免费视频,久久免费视频| 粉嫩av一区二区白浆| 无码人妻一区二区三区蜜桃视频| 揉BBB搡BBB搡BBB| 超碰在线观看免费版| 午夜视频无码| 国产精品尤物| 久艹在线观看视频| 国产一级a毛一级a做免费的视频 | 成人网站www污污污网站公司| 色呦呦视频在线观看| 99er在线观看| 波多野结衣视频免费在线观看 | 浮力影院av| 日韩1234区| 熟女视频网| 操人在线观看| 中文字幕无码Av在线| 无码中文字| 操逼手机视频| 青青国产在线观看| 国产熟女AV| 黄色小视频免费| 东京热av一区二区| jzzijzzij亚洲成熟少妇在线观看 九色蝌蚪9l视频蝌蚪9l视频成人熟妇 | 大荫蒂HD大荫蒂视频| 亚洲69视频| 在线免费A片| 免费三级网站| 蜜臀久久久久久999| 五月婷丁香| 丁香五月激情网| 安徽妇搡BBB搡BBBB户外老太太| 理论片熟女奶水哺乳| 囯产精品久久久久久久久久| 人妻精品一区二区三区| 国产久久在线| 青娱乐超碰在线| 欧美日韩黄色| 黄色精品视频| 91嫩草欧美久久久九九九| 日本无码一区二区三三| 91小电影| 大鸡巴黄色视频免费观看| 国产无码AV大片| 少妇三区| 麻豆视频在线免费观看| 国产亚洲无码| 一区二区中文字幕| 欧洲精品在线免费观看| 国产成人久久精品麻豆二区| 玖玖色资源| 日韩中文字幕在线高清| 大地影视官网第三页入口| 国产精品国产精品国产专区不52| 少妇精品久久久久久久久久| 丁香网五月天| 黄色电影一区二区| 人妻丝袜无码视频专区| 看一级黄色片| 麻豆av在线观看| 中日韩无码| 日韩中文字幕有码| 18禁日韩| 精品无码一区二区三区爱与| 天天久久| 男人av网站| 三级国产网站| 成人免费视频一区| 亚洲AV无码成人H动漫| 国产精品在线免费观看| 东京热一区二区三区四区| 日本精品黄色| 久久久8| 中文字幕无码亚| 欧美一级黃色A片免费看小优视频| 7799精品| 成人网站视频在线观看| 肏屄视频在线观看| 亚洲草片| 狠狠色狠狠操| 亚洲人成77777| 蜜桃AV在线观看| www.199麻豆在线观看网站| 狠狠干网站| 91精品丝袜久久久久久久久粉嫩| 无码理论片| 91久久国产性奴调教| 狠狠干网| 久久99视频| 后入少妇视频| 久久青草影院| 熟女人妻在线视频| 3p视频网站| 久草视频播放| AV中文字幕在线播放| 亚洲精品97久久中文字幕| 一起操在线观看| 亚洲综合激情五月久久| 黑人无码一二三四五区| 一区二区中文字幕| 特级西西WWW无码| 竹菊传媒一区二区三区| 亚洲国产精品二二三三区| 亚洲中文婷婷| 不卡AV在线| 在线黄网| 98在线++传媒麻豆的视频| 中文字幕无码乱伦| 亚洲一二三四区| 91大神在线看| 亚洲精品久久久蜜桃| 中文在线字幕免费观看电视剧大全| 国产美女精品| 一区二区三区久久久| brazzers疯狂作爱| 久久久久久久久成人| 大香蕉在线视频网| 日韩欧美中文字幕在线视频| 黄色视频网站在线免费观看| 国产精品久久久久久久久久两年半| 高清无码在线看| 火淫玖玖免费精品| 天堂成人AV| av手机在线| 一本色道久久综合无码| av资源网站| 中文毛片| 欧美性猛交一区二区三区| 怡红院爽妇网| 黄色一级大片在线免费看国产| 亚洲精品免费在线观看| 日韩中文字幕在线观看视频| 亚洲性爱自拍| 日韩欧美视频| 日本a片免费| 中文字幕乱码中文乱码图片| 日本久热| 一级黄色A片视频| 黄色A片免费视频| 成人四区| 亚洲成人视屏| 亚洲无码在线免费观看| 97人妻碰碰中文无码久热丝袜| 三级国产在线| 天天爱av| 粉嫩99精品99久久久久久夜| 午夜在线免费视频| 亚洲熟妇视频| 伊人9999| 国产内射视频| 91人妻无码精品| 五月天丁香花| 亚洲区成人777777精品| 国产va在线观看| 91中文字幕| 久热在线资源福利站| 狠狠操一区| 在线观看亚洲无码视频| 蜜桃亚洲AV无码一区二区三区| 亚洲无码色| 亚洲一区二区在线免费观看| 成人视频在线免费观看| 丁香五月在线视频| 韩国AV三级| 熟女影音先锋| 特黄特色大片BBBB| 91视频色| 中文无码AV| 欧洲黄色片| 超碰2023| 91人妻人人澡人人爽人人精品乱| 制服丝袜人妻| 国外亚洲成AV人片在线观看| 天天撸天天日| 国产剧情91| 久久草草热国产精品| 日韩人妻无码专区一区二区| 日韩AV网站在线观看| 人妻斩り43歳| 人人干人人操人人摸| 国产777| 成人中文字幕网站| 12—13女人毛片毛片| 操大爷影院| 91一区| 日韩无码第四页| 韩国三级HD久久精品| 亚洲精品成人在线| 亚洲性爱在线| 日韩激情视频在线观看| 乱子伦国产精品视频一级毛| av自拍| 小日本91在线观看| 足交在线播放| 乱子伦国产精品www| 日韩一级片视频| 北条麻妃一区二区三区在线观看| 香蕉视频成人在线观看| 国产99久久九九精品无码免费| 夜夜操影院| 日韩无码黄色片| 日韩在线观看av| 壁特壁视频在线观看| 伊人网在线播放| 亚洲日韩AV电影| 一级a看片在线观看| 91福利导航| aaa少妇| 国产黄片在线视频| 日韩无| 色77777| 精品国产AV色一区二区深夜久久| 理论片无码| 你懂的在线播放| 囯产精品久久久久久久久久久久久久| 无码国产av| 在线免费观看中文字幕| 国产无码一区| 色婷婷在线视频播放| 在线观看日韩av| 黄色A级毛片| 韩无码| 久久精品一二三| 国产Av一区二区三区| 精品无码免费看专区| 玖玖爱这里只有精品| 欧美精品18| 一区二区三区av| 99色天堂| av在线资源观看| 少妇一级| 精品国产久久久久| 五月婷婷丁香在线| 91无码精品一区二区| 欧美干综合| 婷婷狠狠操| 色欲99| 婷婷色大师| 日韩在线视频一区二区三区| 豆花成人社区,视频| 国产精品久久免费视频| 亚洲精品午夜| 国产中文字幕在线播放| 国产污视频| 欧美久久免费| 国产视频中文字幕| 中国老熟女2老女人| 1024在线| 乱伦天堂| 视频一二三区| 91视频专区| 91福利在线观看| 精品一区二区三区四区五区六区七区八区九区| 国产原创精品| 男女拍拍拍| av一区二区在线观看| 欧美一级在线视频| 国产成人AV免费观看| 特写毛茸茸BBwBBwBBw| 欧美97| 婷婷丁香人妻天天爽| 亚洲色色色| 成人无码毛片| 欧美成人一级a片| 一级a一级a爰片免费| 无码三级片在线观看| 双腿张开被9个男人调教| 中文无码一区二区三区| 最新版本日本亚洲色| 天堂色综合| 国产精品久久久久久久久久两年半| 加勒比日韩无码| 熟女人妻在线观看| 狠狠色婷婷7777| 日本黄色大片网站| 无卡无码| 大香蕉最新国产2025| 国产成人精品免费看视频| 成人免费无码婬片在线观看免费| 嫖中国站街老熟女HD| 婷婷综合素质二区| 一区二区三区成人| 97超碰人人摸| 精品av| 日韩一级爱爱| 99热在线只有精品| 免费毛片网站| 性免费网站| 亚洲av在线免费观看| 国产一级电影网站| 精品无码一区二区三区的天堂| 国产福利视频在线观看| 精品国产一级| 欧美亚洲天堂网| 99久久久久久久无码| 色五月在线观看| 91成人片| 亚洲中文字幕日韩|