1、概述
1.1 Java 對象的生命周期
各狀態含義:
- 創建:分配內存空間并調用構造方法
- 應用:使用中,處于被強引用持有(至少一個)的狀態
- 不可見:不被強引用持有,應用程序已經不再使用該對象了,但是它仍然保存在內存中
- 不可達:GC 運行時檢測到(可達性分析)了該對象不再被任何強引用持有,即根不可達
- 收集:被 GC 標記收集準備回收
- 終結:調用該對象的 finalized(),如果 finalized() 內部沒有拯救該對象的措施(即便拯救也只能躲過一次 GC),就會執行回收過程
- 對象空間重新分配:對象已經被回收,其占用空間會被重新分配
1.2 JVM 的堆區內存劃分示意圖
對象在內存區域中流動的大致步驟:
- 對象創建后在 Eden 區
- 執行 GC 后,如果對象仍然存活,則復制到 S0 區
- 當 S0 區滿時,該區域存活對象將復制到 S1 區,然后 S0 清空,接下來 S0 和 S1 角色互換
- 當第 3 步達到一定次數(系統版本不同會有差異)后,存活對象將被復制到 Old Generation
- 當這個對象在 Old Generation 區域停留的時間達到一定程度時,最后會移動到 Permanent Generation 區域
Android 系統使用的虛擬機在 JVM 的基礎上又會多出幾個區域。Dalvik 虛擬機多出:
- Linear Alloc:匿名共享內存
- Zygote Space:Zygote 相關信息
- Alloc Space:每個進程獨占
而 ART 虛擬機多出:
- NonMoving Space
- Zygote Space
- Alloc Space
- Image Space:預加載的類信息(預加載是 Zygote 啟動過程中執行的任務)
- Large Obj Space:分配大對象的區域,如 Bitmap。
此外還需回憶在 Java 專題中講過的:
- 可回收對象的判定:不被 GC roots 直接或間接持有的對象是可回收的,GC roots 包括靜態變量、線程棧變量、常量池和 JNI(指針)
- Java 的四種引用:強>軟(內存不足時回收)>弱(GC 時回收)>虛
- 垃圾回收算法(面試必問):
- 標記清除算法:位置不連續(有內存碎片)、效率略低、兩次掃描(第一次標記,第二次回收)
- 復制算法:實現簡單、運行高效、沒有內存碎片但空間利用率只有一半
- 標記整理算法:沒有內存碎片、效率偏低、兩次掃描(第一次標記,第二次整理)、指針需要調整
- 分代收集算法:未創建新的算法,只是在不同的內存區域使用以上不同的算法
1.3 app 內存組成與限制
Android 系統給每個 app 分配一個虛擬機 Dalvik/ART,讓 app 運行在虛擬機上,即便 app 崩潰也不會影響到系統。系統給虛擬機分配了一定的內存大小,app 可以申請使用的內存大小不能超過此硬性邏輯限制,就算物理內存富余,如果應用超出虛擬機的最大內存就會發生內存溢出。
由程序控制操作的內存空間在堆上,分為 java heapsize 和 native heapsize。Java 申請的內存在 java heapsize 上,如果超過虛擬機的邏輯內存大小就會發生內存溢出的異常;而 native 層的內存申請不受到這個虛擬機的邏輯大小限制,而是受 native process 對內存大小的限制。
通常手機的 RAM 為 4G、8G 甚至 12G,但是每個 app 并不會有太大的內存,通過 adb shell cat /system/build.prop 命令可以看到,虛擬機堆的初始大小為 16M,最大堆內存為 128M:
這個初始大小和最大值,各個手機廠商會自行修改,不同的系統和機型都有可能不同。只不過 Android 系統源碼設置的是 16M,在 AndroidRuntime.cpp 中:
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote){/** The default starting and maximum size of the heap. Larger* values should be specified in a product property override.*/parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m"); //修改這里}
可以看到給 “dalvik.vm.heapsize” 設置的大小為 16M,可以通過修改這個值改變初始的虛擬機堆大小,也可以通過修改 platform/dalvik/+/eclair-release/vm/Init.c 文件:
gDvm.heapSizeStart = 2 * 1024 * 1024; // Spec says 16MB; too big for us.gDvm.heapSizeMax = 16 * 1024 * 1024; // Spec says 75% physical mem
要獲取這個值的話,可以在代碼中通過 AMS 獲取:
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)activityManager.getMemoryClass(); // 以 m 為單位
其實 AMS 是通過在 AMS.setSystemProcess() 內注冊的 meminfobinder 獲取到內存信息的,另外 ActivityManager 中還有 MemoryInfo 這個成員。
此外還可以通過 adb shell cat /proc/meminfo 命令查看內存信息:
1.4 Android 的低內存殺進程機制
oom_adj 在講 AMS 源碼時有講過,可以去復習一下。
AMS 中的 oom_adj 會對應用分級,值為 [-16,15],值越小越不容易被殺,這是一個粗粒度的。此外還有一個 oom_score_adj 評分 [-1000,1000],分越高越容易被殺掉。
通過 adb shell cat /proc/pid/oom_adj 查看 pid 對應的 app 的值,在前臺時為 0,按 home 鍵讓其退到后臺這個值就會變成 11。如果有兩個應用都是 11,那么誰占用的內存大就殺誰,因此【降低應用進入后臺后所占用的內存】也是一種保活方法。
1.5 內存三大問題
內存抖動:內存波動圖形呈鋸齒狀,頻繁 GC 導致卡頓。
內存泄漏:在當前應用周期內不再使用的對象被 GC Roots 引用,導致不能回收,使實際可使用內存變小。
內存溢出:即 OOM,OOM 時會導致程序異常。Android 設備出廠以后,Java 虛擬機對單個應用的最大內存分配就確定下來了,超出這個值就會 OOM。OOM 可以分為如下基幾類:
- Java 堆內存溢出
- 無足夠連續內存
- FD 數量超出限制
- 線程數量超出限制
- 虛擬內存不足
2、常見分析內存的命令
2.1 adb shell dumpsys meminfo
輸出系統內各個應用占用內存信息,以及分類的內存信息:
按照 oom_adj 排序的信息:
按照文件類型分類的信息:
Total RAM 是總的運行內存,Free RAM 是當前可用內存,Used RAM 是當前已使用的內存。
上圖的 PSS 是內存指標概念,與之類似的還有幾個,如下表:
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。
此外還可以通過加 --package 參數查詢某個應用的內存情況,如 adb shell dumpsys meminfo --package packageName:
內存優化時可能會用到這個命令(只是大概判斷,不是精準判斷),比如說在復現前先打印一次內存信息 -> 復現可能的 OOM 操作 -> 再打印一次。
各種其他命令和參數含義去看預習資料 【內存OOM】。
3、常見分析工具
3.1 MAT
按包分類,然后右擊選擇一個類的 incoming references 和 outgoing references,前者表示持有該類實例的對象,后者表示該類持有哪些類的對象。
淺堆(Shallow Heap)與深堆(Retained Heap):前者只計算自身占用的空間,后者則計算本身以及它所引用的對象那一條鏈上的所有對象占用的空間。
比如說下圖:
A~G 每個對象都占 10 個內存單位,它們的淺堆都是 10,但是 B、C 的深堆就要計算上各自分支上的對象總和,即 30,而 A 要計算分支上所有對象的內存總和,即 70。
此時假如新來一個 H 引用 B,那么這時 A 的深堆變為 40,因為假如把 A 干掉的話,能釋放的是 A、C、F、G 這 4 個。新加入的 H 深堆為 10,因為上圖中將 H 干掉就只能釋放它自己一個對象,不會連帶其他對象一起被回收。但是假如在 A 釋放后再看 H 的深堆,那么就是 40,因為這個時候釋放 H 會實際釋放 H、B、D、E 這 4 個對象。
3.2 AS memory profile
看官網連接,介紹的十分詳細:
Inspect your app’s memory usage with Memory Profiler
3.3 LeakCanary
LeakCanary 會找出有泄漏嫌疑的對象,并通過 haha 這個開源庫進行可達性分析確定是否發生了泄漏。它的使用非常簡單,僅需要添加如下依賴:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
然后如果在應用運行時發生了內存泄漏就會在 UI 上提示我們,還可以保存成 hprof 文件交給 MAT 作進一步分析。
LeakCanary 是如何做到僅添加了一個依賴就幫助應用定位內存泄漏問題的呢?
初始化
首先,在 LeakCanary 的部分模塊的 AndroidManifest.xml 中會聲明一些 Provider,這些 Provider 在 apk 打包時會匯入 mergeAndroidManifest.xml,最后體現在 app 的 AndroidManifest.xml 文件中。
由于在 AMS 啟動過程中,會先執行 ContentProvider 的 onCreate(),后執行 Application 的 onCreate():
LeakCanary 正是利用這一點,在 MainProcessAppWatcherInstaller 中初始化:
/*** Content providers are loaded before the application class is created. [MainProcessAppWatcherInstaller] is* used to install [leakcanary.AppWatcher] on application start.** [MainProcessAppWatcherInstaller] automatically sets up the LeakCanary code that runs in the main* app process.*/
internal class MainProcessAppWatcherInstaller : ContentProvider() {override fun onCreate(): Boolean {val application = context!!.applicationContext as ApplicationAppWatcher.manualInstall(application)return true}
}
監聽生命周期
注冊 Application.ActivityLifecycleCallbacks 這個生命周期回調來監聽 Activity 何時被銷毀:
class ActivityWatcher(private val application: Application,private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {private val lifecycleCallbacks =object : Application.ActivityLifecycleCallbacks by noOpDelegate() {override fun onActivityDestroyed(activity: Activity) {reachabilityWatcher.expectWeaklyReachable(activity, "${activity::class.java.name} received Activity#onDestroy() callback")}}override fun install() {application.registerActivityLifecycleCallbacks(lifecycleCallbacks)}override fun uninstall() {application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)}
}
ReferenceQueue
LeakCanary 的核心原理是:一個 Reference 對象(一般使用 WeakReference)所引用的對象被 GC 回收時,這個 Reference 對象會被添加到與之關聯的(一般通過構造方法關聯)引用隊列 ReferenceQueue 的隊列末尾。以下面代碼為例:
public static void main(String[] args) {Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();// 與 ReferenceQueue 關聯WeakReference<Object> weakReference = new WeakReference<>(obj, queue);System.out.println("weakReference: " + weakReference);System.out.println("ReferenceQueue: " + queue.poll());obj = null;// Runtime.gc()一定會執行 GC;而 System.gc() 優先級低,調用后也不知何時執行,// 僅僅是通知系統在合適的時間 GC,并不保證一定執行Runtime.getRuntime().gc();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("weakReference: " + weakReference);System.out.println("ReferenceQueue: " + queue.poll());}
輸出為:
weakReference: java.lang.ref.WeakReference@15db9742
ReferenceQueue: null
weakReference: java.lang.ref.WeakReference@15db9742
ReferenceQueue: java.lang.ref.WeakReference@15db9742
可以證明 WeakReference 持有的對象被回收后,該 WeakReference 對象被添加到了與之關聯的 ReferenceQueue 的隊尾。
LeakCanary 利用這一點去檢測可能的內存泄漏,具體步驟為:
- 給每個對象生成一個 uuid,放到觀察列表 watchedReferences 中,觀察 5 秒
- 5 秒后,調用一次 GC,去 ReferenceQueue 中查找是否有 WeakReference,有說明對象已經被回收,從 watchedReferences 中將其移除。否則該對象沒有被回收,則有可能發生內存泄漏,將其添加到懷疑列表 retainedReferences 中
- 當懷疑列表 retainedReferences 中的元素數量大于 5 個時,就交給開源庫 haha 去做可達性分析
簡版的 LeakCanary 源碼分析看視頻
4、Bitmap 使用
Bitmap 的內存問題解決好就解決了 90% 的 OOM 問題。基本上加載圖片都用的 Glide。不用的話可以參考官網的資料:緩存位圖 等等。
Bitmap 解碼的 decodeXXX() 最終都會去 native 執行。看看預習資料【內存OOM】。
圖片在不同分辨率的設備上的內存中大小可能不一樣。當然這是圖片放在 xxx-xdpi 中的情況,需要根據公式去算 density。但如果圖片來源于網絡或者其他不是 xxx-xdpi 文件夾中的情況,density 就是 1。
gradle 可以控制只打包一個維度的 xxx-hdpi 包,不打其他密度的。
解析 Bitmap 的一個技巧:
try {decode bitmap} catch(OutOfMemoryError e) {對 bitmap 進行質量壓縮}
加入解析 Bitmap 時發生了 OOM,可以在 catch 中通過質量壓縮的方式重新解析該 Bitmap。
第三方開源庫 epic 可以 hook ImageView 設置圖片的過程,檢測圖片的質量。
5、總體優化思路
來自于張紹文的 Android 開發高手課。
總體思想:
- 設備分級:
- Bitmap優化
統一圖片庫
線上線下監控 hook
盡量使用官方推薦的 glide
還可以做一些兜底操作,如果檢測到某個 Activity 發生了內存泄漏,那么可以在它的 onDestroy() 中遍歷所有成員變量和 View 樹并將他們置為 null。
此外在 Activity 中有兩個方法 onTrimMemory() 和 onLowMemory(),分別在 App 內存不足和整個設備內存不足時回調,可以在這兩個方法中主動釋放一些內存。