Android內(nèi)存優(yōu)化三:內(nèi)存泄漏檢測與監(jiān)控

Android內(nèi)存優(yōu)化一:java垃圾回收機制
Android內(nèi)存優(yōu)化二:內(nèi)存泄漏
Android內(nèi)存優(yōu)化三:內(nèi)存泄漏檢測與監(jiān)控
Android內(nèi)存優(yōu)化四:OOM
Android內(nèi)存優(yōu)化五:Bitmap優(yōu)化

Memory Profiler

Memory Profiler 是 Profiler 中的其中一個版塊,Profiler 是 Android Studio 為我們提供的性能分析工具,使用 Profiler 能分析應用的 CPU、內(nèi)存、網(wǎng)絡以及電量的使用情況。

進入了 Memory Profiler 界面。

點擊 Record 按鈕后,Profiler 會為我們記錄一段時間內(nèi)的內(nèi)存分配情況。

image

在內(nèi)存分配面板中,通過拖動時間線來查看一段時間內(nèi)的內(nèi)存分配情況

通過搜索類或者報名的方式查看對象的使用情況

image

使用Memory Profiler 分析內(nèi)存可以查看官網(wǎng):使用內(nèi)存性能分析器查看應用的內(nèi)存使用情況

Memory Analyzer Tool(MAT)

對于內(nèi)存泄漏問題,Memory Profiler 只能提供一個簡單的分析,不能夠確認具體發(fā)生問題的地方。

而 MAT 就可以幫我們做到這一點,它是一款功能強大的 Java 堆內(nèi)存分析工具,可以用于查找內(nèi)存泄漏以及查看內(nèi)存消耗情況。

  1. 使用 Memory Profiler 的堆轉儲功能,導出 hprof(Heap Profile)文件。

as 生成hprof文件無法被mat識別,需要進行轉換

使用hprof-conv進行轉換,hprof-conv位于sdk\platform-tools

// 前一個為as生成的hprof文件,后一個為轉換后的文件
hprof-conv xxx.hprof xxx.hprof 

ps:as導出hprof前最好先gc幾次,可排除一些干擾

  1. 使用mat打開轉換后的文件
image

Histogram 可以列出內(nèi)存中的對象,對象的個數(shù)以及大??; Dominator Tree 可以列出那個線程,以及線程下面的那些對象占用的空間; Top consumers 通過圖形列出最大的object; Leak Suspects 通過MA自動分析泄漏的原因。

  1. Histogram
image

Shallow Heap就是對象本身占用內(nèi)存的大小,不包含其引用的對象內(nèi)存,實際分析中作用不大。常規(guī)對象(非數(shù)組)的ShallowSize由其成員變量的數(shù)量和類型決定。數(shù)組的shallow size有數(shù)組元素的類型(對象類型、基本類型)和數(shù)組長度決定。對象成員都是些引用,真正的內(nèi)存都在堆上,看起來是一堆原生的byte[], char[], int[],對象本身的內(nèi)存都很小。

Retained Heap值的計算方式是將Retained Set(當該對象被回收時那些將被GC回收的對象集合)中的所有對象大小疊加?;蛘哒f,因為X被釋放,導致其它所有被釋放對象(包括被遞歸釋放的)所占的heap大小。

  1. 引用鏈
image

Path To GC Roots -> exclude all phantim/weak/soft etc. references:查看這個對象的GC Root,不包含虛、弱引用、軟引用,剩下的就是強引用。從GC上說,除了強引用外,其他的引用在JVM需要的情況下是都可以 被GC掉的,如果一個對象始終無法被GC,就是因為強引用的存在,從而導致在GC的過程中一直得不到回收,因此就內(nèi)存泄漏了。

image

List objects -> with incoming references:查看這個對象持有的外部對象引用

List objects -> with outcoming references:查看這個對象被哪些外部對象引用

  1. OQL:對象查詢語言

使用對象查詢語言可以快速定位發(fā)生泄漏的Activity及Fragment

select * from instanceof android.app.Activity a where a.mDestroyed = true

select * from instanceof androidx.fragment.app.Fragment a where a.mAdded = false
image

LeakCanary

使用 MAT 來分析內(nèi)存問題,效率比較低,為了能迅速發(fā)現(xiàn)內(nèi)存泄漏,Square 公司基于 MAT 開源了 LeakCanary,LeakCanary 是一個內(nèi)存泄漏檢測框架。

集成LeakCanary后,可以在桌面看到 LeakCanary 用于分析內(nèi)存泄漏的應用。

當發(fā)生泄漏,會為我們生成一個泄漏信息概覽頁,可以看到泄漏引用鏈的詳情。

image

初始化

// 繼承ContentProvider,在應用啟動時,初始化LeakCanary
internal sealed class AppWatcherInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
        val application = context!!.applicationContext as Application
        InternalAppWatcher.install(application)
        return true
}

監(jiān)聽

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

    // 在Activity執(zhí)行onActivityDestroyed時,觀察它的回收狀態(tài)
  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          objectWatcher.watch(
              activity, "${activity::class.java.name} received Activity#onDestroy() callback"
          )
        }
      }
    }

  companion object {

    // 通過application.registerActivityLifecycleCallbacks監(jiān)聽所有Activity的生命周期
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(objectWatcher, configProvider)
      application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

檢測

// #ObjectWatcher

// 在對象可達性發(fā)生更改時,垃圾收集器會將其插入到這個隊列。
private val queue = ReferenceQueue<Any>()

// 受觀察對象的緩存,保存受觀察對象的弱引用
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()

// 1\. 觀察對象
@Synchronized fun watch(
    watchedObject: Any,
    description: String
  ) {

    ...

    // 創(chuàng)建弱引用,watchedObject 為觀察對象,即activity
    val reference =
      KeyedWeakReference(watchedObject,..., queue)

    // 保存受觀察對象的弱引用
    watchedObjects[key] = reference

    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
}

// 將可回收的對象從受觀察對象的緩存中移除
// 當對象變?yōu)槿蹩杉?未被強引用),在最終確定或垃圾回收實際發(fā)生之前,會將WeakReferences入隊
private fun removeWeaklyReachableObjects() {
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      // 從queue 取出的對象為弱可及,表示即將要回收的對象,即未發(fā)生泄漏情況
      // 所以,可以從受觀察對象的緩存中移除它了
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
}

// 2\. 清理一下已回收的對象,如果對象已被回收,則無需再走下面的流程
@Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    // 如果已經(jīng)被回收,則不會存在于緩存中
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {d
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }
// #ObjectWatcher
// 獲取未被回收的對象數(shù)量
val retainedObjectCount: Int
    @Synchronized get() {
        // 清理一下已回收的對象
      removeWeaklyReachableObjects()
      return watchedObjects.count { .. }
    }

# HeapDumpTrigger
private fun checkRetainedObjects(reason: String) {
    val config = configProvider()

    // 3\. 獲取未被回收的對象數(shù)量
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    // 4\. 如果有對象未被回收,執(zhí)行一次GC,然后再獲取一次未被回收的對象數(shù)量
    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

        // 5\. 判斷是否有泄漏,如果有,再判斷是否需要提示
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    ...

        // dump 對內(nèi)存
    dumpHeap(retainedReferenceCount, retry = true)
 }

分析

LeakCanary 會解析 hprof 文件,并且找出導致 GC 無法回收實例的引用鏈,這也就是泄漏蹤跡(Leak Trace)。

泄漏蹤跡也叫最短強引用路徑,這個路徑是 GC Roots 到實例的路徑。

線上監(jiān)控

LeakCanary 存在幾個問題,不同用于線上監(jiān)控功能

  • 監(jiān)控

  • 主動觸發(fā)GC,會造成卡頓

  • 采集

  • Dump hprof,會造成app凍結

  • Hprof文件過大

  • 解析

  • 解析耗時過長

  • 解析本身有OOM風險

線上監(jiān)控需要做的,就是解決以上幾個問題。

各大廠都有開發(fā)線上監(jiān)控方案,比如快手的KOOM,美團的Probe,字節(jié)的Liko

KOOM

快手自研OOM解決方案KOOM今日宣布開源

總結一下幾點:

  • 無主動觸發(fā)GC不卡頓

通過無性能損耗的內(nèi)存閾值監(jiān)控來觸發(fā)鏡像采集。將對象是否泄漏的判斷延遲到了解析時

  • 高性能鏡像DUMP

利用系統(tǒng)內(nèi)核COW(Copy-on-write,寫時復制)機制,每次dump內(nèi)存鏡像前先暫停虛擬機,然后fork子進程來執(zhí)行dump操作,父進程在fork成功后立刻恢復虛擬機運行,整個過程對于父進程來講總耗時只有幾毫秒,對用戶完全沒有影響。

  • hprof分析于裁剪

  • 采用邊緣計算的思路,將內(nèi)存鏡像于閑時進行獨立進程單線程本地分析,不過多占用系統(tǒng)運行時資源;分析完即刪除,不占用磁盤空間;分析報告大小只有KB級別,不浪費用戶流量。

  • 針對鏡像回撈需求,對hprof進行運行時hook裁剪,只保留分析OOM必須的數(shù)據(jù)。裁剪還有數(shù)據(jù)脫敏的好處,只保留對分析問題有用的內(nèi)存中類與對象的組織結構,并不上傳真實的業(yè)務數(shù)據(jù),充分保護用戶隱私。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容