1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        大廠是怎么干掉OOM的?

        共 14997字,需瀏覽 30分鐘

         ·

        2022-05-24 11:29

        不點(diǎn)藍(lán)字關(guān)注,我們哪來故事?


        一個指導(dǎo)程序員進(jìn)入大公司/獨(dú)角獸?的精品社群,致力于分享職場達(dá)人的專業(yè)打法,包括「學(xué)習(xí)路線+簡歷模板+實(shí)習(xí)避坑+筆試面試+試用轉(zhuǎn)正+升職加薪+跳槽技巧」。

        點(diǎn)這里去了解,劍指大廠吧!








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

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

        文章較長,請備好小板凳~

        OOM問題分類

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

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

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

        三、線程數(shù)太多

        3.1 報(bào)錯信息

        pthread_create?(1040KB?stack)?failed:?Out?of?memory

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

        oom1.png

        3.2 源碼分析

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

        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());
        }
        }
        復(fù)制代碼

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

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

        cat /proc/sys/kernel/threads-max

        線程數(shù)限制.png

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

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

        cat proc/{pid}/status

        線程數(shù).png

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

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

        線程優(yōu)化

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

        3.3.1 禁用 new Thread

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

        不過這種方式存在兩個問題:

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

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

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

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

        對于字節(jié)碼操作,在上一篇文章《ASM hook隱私方法調(diào)用,防止App被下架》已經(jīng)詳細(xì)介紹,本文不再過多解釋。

        步驟如下:

        1、創(chuàng)建一個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;
        ????????????}
        ????????}
        ????}
        }
        復(fù)制代碼

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

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

        image.png

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

        image.png

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

        測試代碼

        ????private?fun?testThreadCrash()?{
        ????????Thread?{
        ????????????val?i?=?9?/?0
        ????????}.apply?{
        ????????????name?=?"testThreadCrash"
        ????????}.start()
        ????}
        復(fù)制代碼

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

        線程堆棧.png

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

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

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

        同個場景簡單測試了一下new Thread優(yōu)化前后線程數(shù)峰值對比:

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

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

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

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

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

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

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

        在編譯期,通過ASM,做如下幾個操作:
        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);

        你可以簡單理解為做如下替換:

        詳細(xì)代碼可以參考 booster。

        3.4 線程監(jiān)控

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

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

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

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

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

        具體代碼可以參考:KOOM-thread_holder

        3.4.2 線程上報(bào)

        當(dāng)監(jiān)控到線程有異常的時候,我們可以收集線程信息,上報(bào)到后臺進(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?=?","))
        ????}
        復(fù)制代碼

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

        四、打開太多文件

        4.1 錯誤信息

        E/art:?ashmem_create_region?failed?for?'indirect?ref?table':?Too?many?open?files
        Java.lang.OutOfMemoryError:?Could?not?allocate?JNI?Env
        復(fù)制代碼

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

        4.2 系統(tǒng)限制

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

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

        maxopenfiles.png

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

        ulimit -n

        ulimit.png

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

        cd /proc/10654/fd

        ls

        fd.png

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

        fd2.png

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

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

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

        對于打開文件數(shù)太多的問題,盲目優(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")

        ????}
        復(fù)制代碼

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

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

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

        fd count.png

        當(dāng)懷疑某個文件有問題之后,我們還需要知道這個文件在哪創(chuàng)建,是誰創(chuàng)建的,這個就涉及到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大小、讀寫總耗時

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

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

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

        java?:?FileInputStream?->?IoBridge.open?->?Libcore.os.open?->??
        ?BlockGuardOs.open?->?Posix.open
        復(fù)制代碼

        Libcore.java是一個不錯的hook點(diǎn)

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

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

        復(fù)制代碼

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

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

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

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

        beforeInvoke(method,?args,?throwable);
        result?=?method.invoke(mPosixOs,?args);
        afterInvoke(method,?args,?result);
        復(fù)制代碼

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

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

        4.4.3 Native監(jiān)控方案

        Native Hook方案的核心從 libc.so 中的這幾個函數(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);
        復(fù)制代碼

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

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

        open64
        __read_chk
        __write_chk
        復(fù)制代碼

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

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

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

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

        五、內(nèi)存不足

        5.1 堆棧信息

        內(nèi)存不足.png

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

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

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

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

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

        內(nèi)存不足導(dǎo)致的OOM,一般都是由于Java堆內(nèi)存不足,絕大部分對象都是在堆中分配內(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)致的。(可以參考之前一篇文章分析過其源碼《面試官:簡歷上最好不要寫Glide,不是問源碼那么簡單》)

        基于這個結(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)化,依然可以參考兩年前的一篇文章《面試官:簡歷上最好不要寫Glide,不是問源碼那么簡單》, 文章核心內(nèi)容大概如下:

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

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

        5.3.2 無侵入性自動壓縮圖片

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

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

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

        可以簡單理解如下:

        具體代碼可以參考 McImage 這個庫。

        5.4 ?大圖監(jiān)控

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

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

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

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

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

        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這幾個方法,在這些方法里面,檢測Drawable大小;
        2. 編譯期,修改字節(jié)碼,將所有ImageView的創(chuàng)建都替換成自定義的ImageView;
        3. 為了不影響主線程,可以使用IdleHandler,在主線程空閑的時候再檢測;

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

        debug如下效果:

        大圖上報(bào).png

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

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

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

        LeakCanary

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

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

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

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

        ResourceCanary

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

        具體可以看這篇文章Matrix ResourceCanary -- Activity 泄漏及Bitmap冗余檢測

        KOOM

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

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

        基于KOOM源碼分析

        5.6.1 檢測時機(jī)

        1. 間隔5s檢測一次
        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
        ????????}
        ??????}
        復(fù)制代碼
        • 兩次檢測時間內(nèi)(例如5s內(nèi)),內(nèi)存使用率增加5%

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

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

        所以,作為線上OOM監(jiān)控,dump內(nèi)存鏡像需要單獨(dú)開一個進(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)程,會返回兩次,通過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;
        ??}
        復(fù)制代碼

        注釋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;
        }

        復(fù)制代碼

        注釋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;
        ??}
        }
        復(fù)制代碼

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

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

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

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

        ????kotlin.runCatching?{
        ??????//3、過濾泄漏對象,有幾個規(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)

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

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

        1. 通過shark這個開源庫將hprof文件轉(zhuǎn)換成HeapGraph對象
        2. 收集設(shè)備信息,封裝成json,現(xiàn)場信息很重要
        3. filterLeakingObjects:過濾出泄漏的對象,有一些規(guī)制,例如已經(jīng)destroyed和finished的activity、fragment manager為空的fragment、已經(jīng)destroyed的window等。
        4. findPathsToGcRoot:內(nèi)存泄漏的對象,查找其到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會返回兩次,一次是子進(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)存鏡像信息,這個會比較耗時,執(zhí)行結(jié)束就退出子進(jìn)程;
        5. 子進(jìn)程退出,父進(jìn)程的resumeAndWait就會返回,這時候就可以開啟一個服務(wù),后臺分析內(nèi)存泄漏情況,這塊跟LeakCanary的分析內(nèi)存泄漏原理基本差不多。

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

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

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

        方案如下:

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

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

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

        從map中移除.png

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

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

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

        總結(jié)

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

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

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

        推薦


        歡迎加入我的知識星球,一起劍指大廠,不斷晉升加薪。

        劍指大廠不僅是一個獲取信息的圈子,還是一個規(guī)劃職業(yè)的導(dǎo)師。已在知識星球,更新如下點(diǎn)這里去了解,劍指大廠吧!或點(diǎn)擊下圖了解):


        //////?END?//////
        ↓ 點(diǎn)擊下方關(guān)注,看更多架構(gòu)分享?↓
        瀏覽 46
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            亚洲无码专区在线| 久久国色| 二级黄色视频| 老太色HD色老太HD.| 国产三级偷拍| 日韩高清无码成人| 热无码av| 欧美精品无码久久久精品酒店 | 中文字幕免费av| 五月丁香婷中文字幕| 亚洲播播在线视频| jizz日本护士| 五月色婷婷综合| 一区二区三区三级片| 熟女人妻一区二区三区免费看| 国产黄色视频观看| 国产精品成人国产乱| 天天操人人妻| 少妇无码一区| 特级毛片AAAAAA蜜桃| 日本少妇黄色视频| 日本精品久久| 奇米超碰| 激情另类| 国产骚逼视频| 99热中文字幕在线观看| 中文字幕一区二区三区人妻在线视频| 无码不卡视频在线| 中文字幕日韩无码片| 嫩BBB槡BBBB槡BBB小号| 日本不卡在线观看| 久艹99| 福利大香蕉| 91在线无码精品秘软件| 国产成人精品国内自产拍免费看 | 日韩午夜成人| 免费a片在线观看| 91精品电影18| 久久久久久亚洲| 大香蕉精品在线| 97一区二区| 黄片av| 操噜噜噜噜噜插| 午夜操p| 亚洲中文无码第一页| 久久视频一二| 看欧美黄片| 在线视频三区| 99视频免费在线| 探花在线| 成人免费AV| 99熟女| 日韩av第一页| 91麻豆精品无码| 成人爱爱免费视频| 日韩操逼电影| 天天日天天撸| 无码av免费精品一区二区三区| 色色色五月| 免费一级做a爱片毛片A片小说| 日本操逼网站| 人人看人人摸人人| 77777色| 大乳奶一级婬片A片| 国产精品视频一区二区三| 国产麻豆电影在线观看| 青青热视频| 国产91页| 91蝌蚪在线视频| 俄罗斯老熟妇与子伦| 精品视频日韩| 亚洲自拍小说| 中文在线字幕免费观看| 簧片网站在线观看| 成人AV中文解说水果派| 成人无码网站| 内射少妇18| 一级黄片免费看| AV性爱社区| 久久婷婷影院| 国产wwwww| 另类TS人妖一区二区三区| 91AV在线免费观看| 艹逼在线观看| 国产无码中文| www.亚洲精品| 欧美操操| 日韩中文在线观看| 白浆四溢av| 啪啪啪av| 51成人精品午夜福利| 中文字幕国产在线观看| 午夜黄色视频在线观看| 久久久久成人精品无码| 91免费看片| 国产成人在线免费视频| 天天日天天舔| 亚洲黄色在线观看| 无码三级AV| 午夜一本道| 欧美激情国产精品| 亚洲中文无码电影| 亚洲精品无码a片| 日本高清黄色视频| 麻豆免费成人传媒| 日本精品一区二区| 日韩AA视频| 黄色一级片在线看| 午夜精品久久久久久久91蜜桃 | 亚洲精品高清无码| 蜜臀在线视频| 国产欧美精品在线观看| 国产成人电影一区二区| 艹逼无码| 久久婷婷六月| 国产免费自拍视频| 婷婷五月av| 岛国AV在线| 成人三级黄色| jizz在线观看免费视频| 亚洲午夜无码久久久| 大香蕉免费在线观看| 麻豆视频一区| 成人免费观看视频| 水蜜桃在线视频| 久久久亚洲AV无码精品色午夜| 黄色片在线视频| 亚洲有码在线视频| 狼人综合在线| 亚洲精品成人视频| 91视频一区二区| 无码迷穴| 国产一级AA大片毛片| 国产精品同| 十八禁视频在线观看网站.www | 五月天黄色网| 女人的天堂AV在线观看| 婷婷二区| 免费黄色成人| 国产一区二区三区在线观看免费视频免费视频免费视频 | 天堂网免费视频| 欧美老熟妇BBBBB搡BBB| 日韩一区二区三区精品| 国产精品揄拍500视频| 人人澡视频| 神马午夜秋霞不卡| 人人操人人操人人操| 狼友在线视频| 五月色视频| 欧美日韩在线观看中文字幕 | 怡红院成人av| 一二三区视频| 免费a级毛片| 性爱AV在线观看| 亚洲综合图色40p| 自拍视频一区| 欧美日韩在线视频播放| 天天天天天天干| 亚洲中文字幕人妻。| 欧美一级片网站| 69人妻人人澡人人爽久久| 亚洲一区在线免费观看| 狠狠操综合网| 国产熟女一区二区三区五月婷| 国产精品色情A级毛片| 久久久xxx| 天天干天天日天天色| 欧美日韩激情| 日韩不卡精品| 可以看的黄色视频| 欧美一卡| 亚洲人妻少妇| 新亚洲天堂男子Av-| 大香焦伊人国产| 婷婷午夜精品久久久久久性色| 国产精品主播| 无码人妻一区二区三区| 国产女人精品视频| 色色五月丁香| 人妻精品在线| 永久免费一区二区三区| 午夜无码鲁丝片午夜精品一区二区| 成年视频在线观看| 婷婷久久在线| 男人天堂v| 国产中文字幕AV在线播放| 粉嫩99国产精品久久久久久人妻| 无码人妻熟妇| 91在线无码精品秘蜜桃入口| 中文字幕的| 嫩BBB搡BBBB搡BBBB| 国产在线一| 一卡二卡无码| 蜜桃传媒一区二区| 97精产国品久久蜜桃臀| 亚洲无码性爱视频| 五月天国产精品| 日韩本色一区| 中文字幕在线观看网| 日韩综合在线视频| 亚洲精品高清无码| 久草视频在线资源| 免费的黄片| 亚洲日韩成人电影| 成人在线网| 自拍偷拍福利视频网站| 一级性爱毛片| 欧美激情亚洲| 久久久国产探花视频| 三区在线观看| 欧美高清无码视频| 免费看欧美成人A片无码| 欧美日韩中字| 国产成人精品一区二区三区视频| 操操操影院| 天天色天天日| 国产黄色一级电影| 自拍偷拍综合网| 无码免费高清| 九九热精品视频99| 黑人巨大翔田千里AⅤ| 欧美久久网| 国产非洲欧美在线| 2019天天操| 丁香五月欧美激情| 成人视频网站在线观看18| 91麻豆电影| 激情人妻AV| 亚洲一级在线| 亚洲天堂福利| 日韩中文字幕在线免费观看| 成人免费观看视频| av天天日| 日韩情色| 99免费精品视频| 加勒比在线视频| 国产乱论视频| 一级成人A片| 美女视频一区二区三区| 艹逼91| 无码人妻一区二区三区免费n鬼沢| 日韩字幕久久| 一级免费爱爱| 青青操网| 乳揉みま痴汉电车羽月希免费观看| 蜜臀精品色无码蜜臀AV| 亚洲AV无码一区东京热久久| 久久久大香蕉| 国产一级婬乱片免费| 亚洲AV无码成人| 久久精品免费| 青青草原免费在线视频| 久久精品中文| 懂色av粉嫩AV蜜臀AV| 一区二区三区四区av| 亚洲手机视频| 久久无码黄片| 日韩av第一页| 一本道中文字幕| 国际精品久久久| 免费无码一级A片大黄在线观看| 麻豆91在线| 国产成人综合亚洲| 中文无码熟妇人妻| 日本免费一区二区三区| 成人一级黄色电影| 农村一级婬片A片AAA毛片古装 | 91蝌蚪在线视频| 精品免费国产一区二区三区四区 | 日本肏逼视频| 人人妻人人超| 国产高清无码在线| 性猛交AAAA片免费观看直播| 国产乱伦对白| 日韩成人在线观看| 丁香综合网| 亚洲中文字幕码mv| 伊人日韩| 国产人妻在线| 亚洲精品区| www.豆花社区成人| 五月天欧美性爱| 日韩一区二区三免费高清在线观看| 三浦恵子一级婬片A片| 米奇色色| 精品国产久久久久| Chinese搡老女人| 无码迷穴| 操逼网五月天| 亚洲视频免费在线| 伊人AV在线| 操逼逼网站| 99热视| 爱爱日韩| 人妻天天爽| 无码免费中文字幕| 欧美性爱69| 少妇一级| 免费观看亚洲视频| 四虎黄色网址|