亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

本文通過一類 Android 機型上相機拍攝過程中的 native 內(nèi)存 OOM 的問題展開,借助內(nèi)存快照裁剪回撈和 Native 內(nèi)存監(jiān)控工具的賦能,來深入剖析此類問題。

背景

Raphael 是西瓜視頻 Android 團隊開發(fā)的一款 native 內(nèi)存監(jiān)控工具,在字節(jié)跳動內(nèi)部產(chǎn)品(如西瓜、抖音、頭條等)上廣泛用于監(jiān)控 native 內(nèi)存泄漏問題。在抖音 7.8.0-8.3.0 上搜集到大量因虛擬內(nèi)存觸頂而 crash 的內(nèi)存日志現(xiàn)場(如 pthread_create、GL error、EGL_BAD_ALLOC),其中 60%以上都是 camera 相關(guān)的內(nèi)存泄漏,占整體 crash 的 15%以上(JAVA & Native)。同時也收到 OPPO 等廠商反饋抖音 App 在其新機型上 native crash 比其他機型高了 3 倍以上,分析廠商提供的日志發(fā)現(xiàn)基本都是虛擬內(nèi)存觸頂導(dǎo)致的 carsh,這其中 80%以上都有 camera 相關(guān)的內(nèi)存分配失敗的日志。

問題

通過對 native 內(nèi)存監(jiān)控搜集到的日志進行堆棧聚合和 so 級的內(nèi)存占用統(tǒng)計,可以發(fā)現(xiàn)截止到 OOM 時工具攔截到的 native 內(nèi)存總量已經(jīng)達到了 1.3G 左右(32 位下應(yīng)用可直接使用的 native 內(nèi)存上限約 2G),這其中占比最大的是 CameraMetaData 對象間接引用的內(nèi)存,native 內(nèi)存泄漏十分嚴重。

Android Camera 內(nèi)存問題剖析

 

由于 native 內(nèi)存分配的頻率過高,獲取 Java 層堆棧又比較耗時,在攔截 native 內(nèi)存分配時并不適合直接頻繁抓取 Java 堆棧。Native 內(nèi)存不同于 Java 內(nèi)存,單從攔截到的數(shù)據(jù)很難直觀給出結(jié)論。通常對于內(nèi)存等資源不合理使用導(dǎo)致的資源不足而引發(fā)的問題都很難歸因,從攔截到的數(shù)據(jù)來看,CameraMetaData 所引用的內(nèi)存最大,嫌疑也最大,基于此決定剖析一下這個問題

初步分析

分析 native 內(nèi)存的分配和釋放

通過攔截到的堆棧可以看出,CameraMetaData 的創(chuàng)建堆棧的上層是 Java 調(diào)用,最終在 native 層進行的內(nèi)存分配(boot-framework.oat & libandroid_runtime.so)。CameraMetaData 對象有兩部分內(nèi)存,對象本身 & mBuffer 指向的 camera_metadata_t 所引用的內(nèi)存;通過源碼可知,每個 CameraMetadata 對象的 mBuffer 所指向的 camera_metadata_t 是獨立的,彼此是不重疊的。

Android Camera 內(nèi)存問題剖析

 


Android Camera 內(nèi)存問題剖析

 

既然工具能攔截到這么多的未釋放的內(nèi)存分配,一定是因為這些內(nèi)存的釋放邏輯出問題導(dǎo)致的,我們需要優(yōu)先調(diào)查清楚 CameraMetadata.mBuffer 的釋放邏輯。通過分析 CameraMetadata.cpp 的源碼可知,CameraMetadata::release()并未釋放 mBuffer 所指向的內(nèi)存,而是把 mBuffer 所指向的內(nèi)存賦值給了另一個 CameraMetadata 對象;CameraMetadata::clear()是真釋放,而 clear 的調(diào)用有兩個場景:一個是在 camera_metadata_t 復(fù)用時,另一個是 CameraMetadata 對象析構(gòu)時。

Android Camera 內(nèi)存問題剖析

 

前述結(jié)論可知 CameraMetadata.mBuffer 所指向的 camera_metadata_t 是彼此獨立的。通過工具攔截到的堆棧和分配數(shù)量猜測,Native OOM 時內(nèi)存中一定存在大量的 CameraMetadata 實例。C++對象的析構(gòu)通常是調(diào)用 delete 來實現(xiàn)的,AOSP 里想搜索哪里 delete 了一個 CameraMetaData 對象是很難的,因為很難知道 delete 時的變量名。根據(jù)一個基本的 C++編程規(guī)范,內(nèi)存通常在哪里創(chuàng)建的,應(yīng)該就在那里釋放,我們?nèi)炙阉?new CameraMetaData 字符串就可以很輕松的發(fā)現(xiàn) CameraMetaData 對象的創(chuàng)建和釋放均是在/frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp里實現(xiàn)的。

Android Camera 內(nèi)存問題剖析

 


Android Camera 內(nèi)存問題剖析

 


Android Camera 內(nèi)存問題剖析

 

通過 android_hardware_camera2_CameraMetadata.cpp 里的注冊清單可以看到與這些函數(shù)關(guān)聯(lián)的 Java 層 class 是android/hardware/camera2/impl/CameraMetadataNative,CameraMetadata_close 函數(shù)在 Java 對應(yīng)的是 nativeClose 函數(shù)。可以進一步發(fā)現(xiàn) CameraMetaDataNative 里 nativeClose 函數(shù)是在 close 函數(shù)里調(diào)用的,而 close 函數(shù)又是在 finalize 函數(shù)調(diào)用的。

Android Camera 內(nèi)存問題剖析

 


Android Camera 內(nèi)存問題剖析

 

通過上述分析可知只有在 CameraMetaDataNative 對象執(zhí)行 finalize 方法時才會回收與之對應(yīng)的 native 內(nèi)存,而 finalize 方法又是在 FinalizerDaemon 線程里執(zhí)行的,猜測到如果發(fā)生了上述堆棧的 native OOM,Java 層一定存在大量還沒有執(zhí)行 finalize 方法的 CameraMetaDataNative 對象。

排查 Java 堆現(xiàn)場

幸運的是我們通過內(nèi)存快照裁剪工具(Tailor)輕松拿到了大量這類 native OOM 時對應(yīng)的 Java 堆內(nèi)存快照文件。這些內(nèi)存快照文件完美證實了之前的猜想,當(dāng)發(fā)生這類 native OOM 時 Java 層的確存在大量的 CameraMetadataNative 對象。以下圖為例,這些 CameraMetadataNative 對象里除 6 個被其他代碼引用外,其余對象全部在 FinalizerDaemon 線程的隊列里,等待執(zhí)行 finalize 方法。同時,快照里有 6658 個對象,只有大約 600+對象的 mMetadataPtr 是等于 0 的,說明這部分對象對應(yīng)的 Native 內(nèi)存需要在 finalize 時釋放,這跟工具攔截的數(shù)據(jù)是完全匹配的,也間接驗證了 Native 內(nèi)存監(jiān)控的正確性和可靠性

Android Camera 內(nèi)存問題剖析

 

深入分析

排查 Finalize 執(zhí)行

雖然上述分析驗證了問題,也證實了之前的猜想,但仍未找到導(dǎo)致此類問題的深層次原因,對于最終解決此類問題也仍然束手無策。為什么會有這么多的 CameraMetadataNative 對象等待執(zhí)行 finalize 方法或許是下一步的調(diào)查方向。做過 Java 穩(wěn)定性治理的同學(xué)應(yīng)該都知道一類很有名的 TimeoutException 異常,這類異常的根本原因是 finalize 執(zhí)行超時導(dǎo)致的,這個 case 會不會是某個對象的 finalize 執(zhí)行超時導(dǎo)致的?

Android Camera 內(nèi)存問題剖析

 

結(jié)合 FinalizerDaemon 的源碼可以看到,每執(zhí)行一個對象的 finalize 方法時,都會通過finalizingObject屬性記錄當(dāng)前的對象。如果真的是 finalize 超時導(dǎo)致的,一定存在 finalizingObject 屬性不為空的現(xiàn)場。我們在遍歷完所有相關(guān)內(nèi)存快照里的 FinalizerDaemon 線程狀態(tài)后發(fā)現(xiàn),這些現(xiàn)場的 finalizingObject 屬性均為空。這個結(jié)果很意外,似乎并不是某個對象的 finalize 方法執(zhí)行超時導(dǎo)致的。

Android Camera 內(nèi)存問題剖析

 

通過分析finalizingReference = (FinalizerReference<?>)queue.remove() 發(fā)現(xiàn)這行代碼后面的邏輯并沒有對 finalizingReference 判空,說明這個地方一定不會返回空。既然不為空, queue.remove() 只能 block 等待,這個 ReferenceQueue.java 的源碼也證實了猜想。

Android Camera 內(nèi)存問題剖析

 

源碼顯示 goToSleep 是個同步方法,可能會 block。但遍歷所有相關(guān)快照發(fā)現(xiàn)所有的 needToWork 屬性均是 false,證明已經(jīng)走過(只有FinalizerWatchdogDaemon.INSTANCE.goToSleep() 會置為 false,而且這個函數(shù)是 private 的,只在 FinalizerDaemon 線程里調(diào)用),所以 block 在這里的可能性幾乎沒有。

Android Camera 內(nèi)存問題剖析

 


Android Camera 內(nèi)存問題剖析

 

其實 block 在這里的原因通常是因為只有在 GC 時才會將需要執(zhí)行 finalize 的對象加入到 FinalizerDaemon 的隊列里。如果一段時間內(nèi)沒有 GC,且隊列就為空時,上面的 remove 會一直 block,直到 GC 后才有對象加入到這個隊列里。巧合的是我們在發(fā)生這類 native OOM 時會通過 Tailor 主動 dump Java 堆的內(nèi)存快照,而 dump 快照時會觸發(fā) GC & suspend,這個最終導(dǎo)致大量的 CameraMetadataNative 對象被同時加入到 FinalizerDaemon.queue 的隊列里。

分析 GC 策略

通過上述分析可知如果不是 GC,這些對象是不會被被加入到 FinalizerDaemon.queue 里的,這說明這類 native OOM 發(fā)生前的一段時間內(nèi)一直沒有 GC,才導(dǎo)致大量 CameraMetadataNative 對象沒有及時執(zhí)行 finalize,進而發(fā)生 native OOM。以上分析也在線下進入到拍攝頁后靜置觀察實驗中得到驗證,這其中大概每隔 30s-40s 甚至更長時間 Java 堆才會主動觸發(fā)一次 GC,在這期間 native 內(nèi)存會不斷增長,直到 GC 后才會大幅下降,Java & Native 內(nèi)存才會恢復(fù)到正常水平。雖然問題不是 block 在 finalize 環(huán)節(jié),但最終這個問題的原因被鎖定在了 GC 邏輯上!

Android Camera 內(nèi)存問題剖析

 


Android Camera 內(nèi)存問題剖析

 

了解 GC 的同學(xué)可能會知道 ART 虛擬機的 GC cause 有很多種,kGcCauseForAlloc/kGcCauseBackground 是虛擬機最易頻繁觸發(fā)的。當(dāng)停留在拍攝頁不做任何操作時,程序邏輯相對簡單,這期間只有相機服務(wù)周期(>=30 次/s)地通過 binder 在應(yīng)用端觸發(fā)創(chuàng)建 CameraMetadataNative 對象,并在拍攝頁顯示一張相機采集到的圖像。這個過程 Java 堆只有 CameraMetadataNative 對象創(chuàng)建,而 CameraMetadataNative 自身占用內(nèi)存比較小,一次 GC 之后 Java 堆內(nèi)存比較富裕的情況下,虛擬機很長一段時間內(nèi)不會主動觸發(fā) GC。如果這期間 native 內(nèi)存的增幅過大,在下次 GC 之前觸頂就發(fā)生 native OOM

Android Camera 內(nèi)存問題剖析

 

綜上,這類 native OOM 的根本原因是:當(dāng)應(yīng)用自身的 native 內(nèi)存本身已處于高水位時,開啟相機后,相機服務(wù)會持續(xù)通過 binder 通信在應(yīng)用側(cè)創(chuàng)建 CameraMetadataNative 對象,創(chuàng)建 CameraMetadataNative 對象的同時也會在應(yīng)用側(cè)通過 jni 接口在 native 層創(chuàng)建/復(fù)用一塊存放 camera_metadata_t 的相對比較大的內(nèi)存。由于 Java 層的 CameraMetadataNative 對象本身比較小,這種連續(xù)創(chuàng)建小對象的行為一定時間內(nèi)很難觸發(fā) Java 層的 GC,導(dǎo)致其間接引用的 native 內(nèi)存不斷上漲,最終觸發(fā)虛擬內(nèi)存上限而 crash。

解決思路

問題的原因雖然相對比較簡單,但如何解決這類問題還是比較難抉擇的。既然是 GC 不及時導(dǎo)致的,一種簡單的方案就是在拍攝頁周期性觸發(fā) GC。但如果 GC 間隔比較小,GC 畢竟是耗時的,GC 過于頻繁會嚴重影響拍攝體驗;如果 GC 間隔時間比較長,還是會有大概率重蹈這類 native OOM 的覆轍。

主動觸發(fā) GC 的方案很難平衡對性能的影響。其實問題的重點不是 Java 層,而是 Java 對象引用的 native 內(nèi)存,如果及時主動釋放這部分內(nèi)存就可以從根本上徹底解決此類問題。通過前面的分析可以知道,這部分內(nèi)存原本是在 GC 時的 finalize 環(huán)節(jié)回收,但如果提前發(fā)現(xiàn) CameraMetadataNative 不再使用時,主動觸發(fā)來釋放這部分內(nèi)存就可以一勞永逸。通過分析源碼可以發(fā)現(xiàn) CameraMetadataNative 傳遞到應(yīng)用層之后后續(xù)并未再使用,在應(yīng)用層使用完 CameraMetadataNative 對象之后,通過反射調(diào)用 close 函數(shù)即可釋放其所引用的 native 內(nèi)存。

Android Camera 內(nèi)存問題剖析

 

線下實驗也可以發(fā)現(xiàn),開啟主動回收策略后,Native 內(nèi)存的增長速度比之前大幅降低。這期間 Java 堆& native 層仍有持續(xù)增加的小對象,但 native 的增長速度遠小于 Java 層了,這種場景下 Java 內(nèi)存會在 native 內(nèi)存觸頂之前先觸發(fā) GC,而大幅降低了發(fā)生 native OOM 的可能

Android Camera 內(nèi)存問題剖析

 

最終該方案上線后,效果十分明顯,此類 crash(Java & Native 總占比>15%)基本清零。后續(xù)搜集到的內(nèi)存監(jiān)控日志里 CameraMetadata 相關(guān)的內(nèi)存基本都在 2M 以內(nèi),效果立竿見影!

總結(jié)

此類問題存在時間很久,至少從 Android 4.4 開始都是通過 CameraMetadataNative 的 finalize 函數(shù)來釋放 native 內(nèi)存。過去拍攝的需求比較簡單,絕大多數(shù)時候都是使用 ROM 自帶的相機應(yīng)用來拍照,因為這類 app 比較簡單,native 內(nèi)存水位本身很低,很難觸發(fā)到虛擬內(nèi)存的上限,所以此類問題并沒暴露出來。隨著小視頻等 app 的興起,拍攝需求越來越重(特效&美顏等),app 也越來越復(fù)雜,應(yīng)用自身的 native 內(nèi)存水位不斷上漲,加上 native 內(nèi)存泄漏等原因,當(dāng)長時間停留在拍攝頁時,這類問題就很容易觸發(fā)。

此外,CameraMetadata 的內(nèi)存分配失敗時,并不會直接 crash,這個時候有其他內(nèi)存分配請求時才會觸發(fā) crash(如線程創(chuàng)建、GL 內(nèi)存分配等),這也是很多拍攝過程中相機黑屏問題的根本原因。該方案也不經(jīng)意間解決了長期存在的拍攝時相機黑屏的疑難問題。

這類問題既有應(yīng)用自身的原因,也有內(nèi)存回收策略設(shè)計的原因。應(yīng)用在盡可能減少泄漏的同時,也應(yīng)該努力降低自身 native 內(nèi)存水位。AOSP 里利用 Java 的 finalize 方法來釋放其間接引用的 native 內(nèi)存是個偷懶挖坑的設(shè)計,類似的案例在 AOSP 里比比皆是。我們在實際開發(fā)中,類似內(nèi)存這種有限的資源應(yīng)及時回收,甚至可以主動限定對象的生命周期,一旦完成使命就主動回收其占用的內(nèi)存,避免使用 finalize 邏輯來釋放 native 內(nèi)存。

文中提高的兩個工具(Native 內(nèi)存監(jiān)控工具 Raphael & Android 堆內(nèi)存快照裁剪壓縮工具)是西瓜視頻 Android 團隊在長期的內(nèi)存優(yōu)化治理中開發(fā)的兩套高效實用的基礎(chǔ)工具,在我司內(nèi)部各大 app 中應(yīng)用非常廣泛,是內(nèi)存優(yōu)化&穩(wěn)定性治理的絕對首選。這兩套工具我們也會在后續(xù)的監(jiān)控工具建設(shè)&優(yōu)化治理實踐等技術(shù)文章中介紹相關(guān)技術(shù)細節(jié),敬請關(guān)注。

更多分享

字節(jié)跳動自研線上引流回放系統(tǒng)的架構(gòu)演進

字節(jié)跳動表格存儲中的事務(wù)

iOS大解密:玄之又玄的KVO

今日頭條 Android '秒' 級編譯速度優(yōu)化

字節(jié)跳動-西瓜視頻 Android 團隊

字節(jié)跳動-西瓜視頻 Android 團隊是負責(zé)字節(jié)跳動旗下西瓜視頻 App 研發(fā)的客戶端團隊,團隊在滿足業(yè)務(wù)高速迭代的同時,持續(xù)優(yōu)化性能和體驗,提升研發(fā)效率,探索 Flutter 等跨平臺方案。我們長期招聘業(yè)務(wù)研發(fā)、架構(gòu)師、Flutter 工程師、骨干工程師、實習(xí)生,在北京、杭州、上海三地均有職位。業(yè)務(wù)體量大,團隊成長快,技術(shù)挑戰(zhàn)大,歡迎各路人才加入!聯(lián)系郵箱: [email protected] ;郵件標題:姓名-工作年限-西瓜-Android

分享到:
標簽:Android
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定