0. 環境:
電腦:Windows10
Android Studio: 2024.3.2
編程語言: Java
Gradle version:8.11.1
Compile Sdk Version:35
Java 版本:Java11
1.什么是OOM
OOM即 OutOfMemoryError 內存溢出錯誤。常見于一些
- 資源型對象未關閉
- 注冊對象未注銷(反注冊)
- 類的靜態變量持有大量數據對象
- 單例造成的內存泄漏
- 非靜態內部類的靜態實例
- Handler臨時性內存泄漏
- 容器中的對象沒清理造成的內存泄漏
- WebView
- 使用ListView時造成的內存泄漏
等導致的。
Android內存泄漏常見場景及解決方案
- 資源性對象未關閉
????????常見場景:例如:Bitmap,使用后就不管了。
????????解決方案:對于資源性對象不再使用時,應該立即close(),然后設置為null。例如Bitmap。如果未關閉則容易造成內存泄漏。最好是在activity中的onDestroy()中將Bitmap給close()并設置成null。
- 注冊對象未注銷
????????常見場景:例如:BroadcastReceiver、EventBus未注銷造成的內存泄漏。
????????解決方案:應該在activity的onDestroy()中及時注銷/反注冊
- 類的靜態變量持有大數據對象
????????常見場景:靜態變量存儲數據
????????解決方案:盡量避免使用靜態變量存儲數據。如果是大數據對象,建議使用數據庫存儲。
????????關于數據庫,可以查看我的這篇文章:【安卓筆記】Room數據庫的基本使用-CSDN博客
- 單例造成的內存泄漏
????????常見場景:使用了Activity的Context,但是被外部持有,導致無法回收。
????????解決方案:優先使用Application的Context,如需使用Activity的Context,可以在傳入Context時使用弱引用進行封裝,然后在使用到的地方從弱引用中獲取Context。如果獲取不到,則return即可。
- 非靜態內部類的靜態實例
????????常見場景:實例化了 非靜態內部類的靜態實例
????????解決方案:該實例的生命周期和應用一樣長,這就會導致該靜態實例一直持有該Activity的引用,activity的內存資源不能正常回收。此時,我們可以將該內部類設置為靜態內部類或者將該內部類抽取出來封裝成一個單例,如果需要使用Context,盡量使用Application的Context。如果一定要使用Activity的Context,記得用完后要置空,讓GC可以回收。
- Handler臨時性內存泄漏
????????常見場景:Handler是非靜態,并且創建在Activity或者Service中。
????????解決方案:Message發出之后存儲在MessageQueue中。在Message中存在一個target,它是Handler的一個引用。Message在Queue中存在的時間過長,就會導致Handler無法被回收。如果Handler是非靜態的,則會導致Activity或者Service不會被回收。并且消息隊列實在一個Looper線程中不斷地輪詢處理消息。當這個Acitivity退出時,消息隊列中還有未處理的消息或者正在處理的消息,并且消息隊列中的Message持有Handler實例的引用,Handler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。
- 容器中的對象沒清理造成的內存泄漏
????????解決方案:在退出程序之前,將集合.clear(),然后設置為null,再退出程序。
- WebView
????????常見場景:WebView大多都存在內存泄漏的問題。
????????解決方案:在應用中只要使用一次WebView,內存就不會被釋放掉。我們可以為WebView開啟一個單獨的進程,使用AIDL與應用的主進程進行通信。WebView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,達到正常釋放內存的目的。
- 使用ListView時造成的內存泄漏
????????解決方案:在構造Adapter時,使用緩存的convertView
OOM比較難找到問題的根源,就是因為大部分OOM的問題,都是內存先被吃完了,最后由正常代碼引發OOM,所以,日志中基本上只能看到最后的觸發導致。這往往不是根本原因。
OOM的原因分類:
1. Java堆內存溢出(內存不夠)
2. 無足夠連續內存空間(內存夠,但大部分都是碎片化)
3. FD數量超出限制(文件句柄(FD: 文件句柄)泄漏)
4. 線程數量超出限制(線程數量泄漏)
5. 虛擬內存不足
2. 一些散裝知識
Java的對象生命周期
Java的對象生命周期(Java Object Life Cycle)(有大概了解就行了)
Created 創建
in use 應用
Invisible 不可見
Unreachable 不可達
Collected 收集
Finalized 終結
Deallocated 對象空間重新分配??
Java的四種引用
強引用:=
????????強引用:被GC掃描到了,也不會回收。容易造成OOM
軟引用:SoftReference
????????軟引用:內存不足時,會回收
弱引用:WeakRefoerence
????????弱引用:GC掃描到了,會被回收掉
虛引用:PhantomReference?
????????虛引用:相當于沒有被引用
3. Android內存分析命令
- dumpsys meminfo
- procrank
- cat/proc/meminfo
- free
- showmap
- vmstat
- top -n 1
3.0 一些條件
1. 手機需要root(Android模擬器也行)
2. 需要使用adb
3. 進入 adb shell,再操作以下命令
3.1?dumpsys meminfo
功能:大概判斷哪個頁面有內存泄漏
內存指標概念
item | 全稱 | 含義 | 等價 |
USS | Unique Set Size | 物理內存 | 進程獨占的內存 |
PSS | Proportional Set Size | 物理內存 | PSS = USS + 按比例包含共享庫 |
RSS | Resident Set Size | 物理內存 | RSS = USS + 包含共享庫 |
VSS | Virtual Set Size | 虛擬內存 | VSS = RSS + 未分配實際物理內存 |
總結:VSS >= RSS >= PSS >= USS,但 /dev/kgsl-3d0部分必須考慮VSS
一般只看PSS。
這邊說一下如何判斷activity中的view是否有內存泄漏:
例如從Aactivity進入Bactivity,再返回再進入,多次之后。
使用dumpsys meminfo命令,在命令提示符窗口最下方找到Objects組,查看ViewRootImpl數據,如果該數據在每次activity進入時都會增加。則代表該activity持有的view內存泄漏了
3.2 procrank
功能:獲取所有進程的內存使用情況,順序以PSS的大小從大到小排列。
procrank比dumpsys meminfo,更詳細輸出 VSS/RSS/PSS/USS內存指標。
例如:最后一行輸出以下6個指標:
total | free | buffers | cached | shmem | slab |
2857032K | 998088K | 78060K | 78060K | 312K | 92392K |
3.3 cat /proc/meminfo
功能:查看更加詳細的內存信息
3.4 free
功能:查看可用內存。單位KB。
該命令比較簡單、輕量,專注于查看剩余內存的情況。數據來源于3.3 cat /proc/meminfo
3.5 showmap
?功能:用于查看虛擬地址區域的內存情況
用法:
showmap -a [pid]
- start addr和end addr:?分別代表進程空間的起止虛擬地址
- virtual size/RSS/PSS:具體看3.1中的介紹
- shared clean:代表多個進程的虛擬地址可指向這塊物理空間
- shared:共享數據
- private:該進程私有數據
- clean:干凈數據,該內存數據與disk數據一致。當內存緊張時,可直接釋放內存,不需要回寫到disk
- dirty:臟數據,需要回寫到disk,才能被釋放。?
3.6 vmstat
功能:不僅可以查看內存情況,還可以查看進程運行隊列、系統切換、CPU時間占比等。(可以周期性動態輸出)
用法:
vmstat [ -n iterations ] [ -d delay ] [ -r header_repeat ]
解釋:
-n iterations:數據循環輸出的次數
-d delay:兩次數據間的延遲時長(單位:s)
-r header_repeat:循環次數
3.7 top -n 1?
op命令是Linux下常用的性能分析工具,可以實時顯示系統中各個進程的資源占用情況。
功能:顯示當前系統正在執行的進程的相關信息:進程ID、內存占用率、CPU占用率等
用法:
top [參數]
參數:
-b 批處理
-c 顯示完整的治命令
-l 忽略失效過程
-s 保密模式
-S 累積模式
-i<時間> 設置時間間隔
-u<用戶名> 指定用戶名
-p<進程號> 指定進程
-n<次數> 循環顯示的次數?
3.8 總結
1. dumpsys meminfo 適用場景:查看進程的oom adj、dalvik/native等區域內存使用情況、某個進程/apk的內存情況
2. procrank 適用場景:查看進程的VSS/RSS/PSS/USS
3. cat /proc/meminfo 適用場景:查看系統的詳盡內存信息,包含內核情況
4. free 適用場景:只查看系統的可用內存
5. showmap 適用場景:查看進程的虛擬地址空間的內存分配情況
6. vmstat 適用場景:周期性打印進程運行隊列、系統切換、CPU時間占比等情況
4. 常見分析工具
1. Memory Analyzer Tools
? ? ? ? 開發過程中,本地分析
2. Memory Profiler(本文不介紹)
? ? ? ? Android Studio 自帶的分析工具
3. LeakCanary(本文不介紹)
? ? ? ? 線上版本集成該工具分析
5. MAT(Memory Analyzer Tools)工具的使用
工具下載地址:Downloads | The Eclipse Foundation
5.1 .hprof文件分析
????????5.1.1?儲存.hprof文件到手機內
public static void createDumpFile(Context context) {// 目錄路徑String LOG_PATH = "/dump.gc/";// 文件名稱SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ssss");String createTime = sdf.format(new Date(System.currentTimeMillis()));// 手機環境String externalStorageState = Environment.getExternalStorageState();// 如果有SD卡if (Environment.MEDIA_MOUNTED.equals(externalStorageState)) {// 文件路徑File file = new File(Environment.getExternalStorageDirectory().getPath() + LOG_PATH);if (!file.exists()) {// 沒有該路徑就創建file.mkdirs();}String hprofPath = file.getAbsolutePath();if (!hprofPath.endsWith("/")) {hprofPath += "/";}// 文件名稱hprofPath += createTime + ".hprof";try {// dump儲存.hprof文件android.os.Debug.dumpHprofData(hprofPath);} catch (IOException e) {throw new RuntimeException(e);}}
}
在執行完需要查看是否OOM的代碼后,再調用該函數,就可以在 手機路徑下看到.hprof文件:
sdcard/dump.gc/yyyy-MM-dd_HH.mm.ssss.hprof
????????5.1.2??MAT工具打開文件
此時無法用MAT工具直接打開文件,需要轉換。轉換的命令如下:
hprof-conv dump1.hprof converted-dump2.hprof
解釋
dump1.hprof:轉換前的文件
dump2.hprof:轉換后的文件
如果上面命令無效,請使用以下命令:
hprof-conv.exe dump1.hprof dump2.hprof
解釋:
dump1.hprof:轉換前的文件
dump2.hprof:轉換后的文件
(提一嘴,該命令的環境路徑在 android/sdk/platform-tools 下)?
轉換后就可以打開了。?
????????5.1.3 查看.hprof文件
其中 Problem Suspect 為工具猜想泄漏可能。不重要
首先點擊柱狀圖,按對象查看,出現下面的列表:(柱狀圖右側的按鈕,自行測試一下就好了。例如:按線程查看)
然后第二步選擇按package
?選擇完這個后,界面就會變成如下:
這樣,就可以比較直觀、比較友好的查看包名下的對象了。
????????5.1.4 具體對象查看?
我們在該頁面下,右鍵鼠標,選擇List objects后,右側有兩個選項,如下圖:
with outgoing references
查看該對象持有誰
with incoming references?
查看誰持有該對象?
?5.2 MAT中淺堆和深堆
Shallow Heap:淺堆
對象本身占用的內存
Retained Heap:深堆
統計結果:本身占用內存 + 引用的對象占用內存
例如下圖:
假設ABCDEFG各占用10個內存大小。但是A持有BC,B持有DE,C持有FG。所以A的淺堆為自身:10,深堆為70(A自身+BDE引用 +CFG引用)?
在工具中,就是這一部分數據:
?5.2.1 找到深堆異常
通過對深堆的查看,就可以知道 哪個對象占用內存過大。然后通過incoming和outging的引用關系查看,就可以查詢上下關系,找到內存泄漏的點。例如某個對象被多處引用,被持久化導致無法釋放。