天天看點

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程序間的記憶體配置設定應用記憶體管理參考

Android 高手核心知識點筆記(不斷更新中🔥)點選檢視 PS:各位童鞋不要忘記給我 star 一波哦~~

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考

衆所周知 Android 以 Java 、Kotlin 為程式設計語言,在編譯時 Anrdoid 會将代碼産出的位元組碼檔案經優化後形成.dex 檔案儲存于 Apk 中,而 Android 中所有的應用程式都運作在 Android 虛拟機中。那麼既然都已 Class 檔案為原材料、以虛拟機為運作載體 Android 虛拟機與 JVM 在記憶體管理方法有什麼差異呢?

JVM vs Android VM

在前面的文章已經對 JVM 記憶體管理相關的内容作了介紹,在此我們總結一下 JVM 的特點。

JVM 是一個介于 Java 程式與運作平台之間的一個抽象層,JVM 是依賴具體平台的,不同的平台都有自己的實作。

JVM

例如,你有一個 HelloWorld.java 檔案,當你想運作他的時候,需要使用 javac 工具将其編譯為 Bytecode 位元組碼檔案,這意味着 javac 并不會和其他編譯器一樣将程式直接編譯為機器碼。Bytecode 是一個二進制檔案,裡面的内容都是以8個位元組為寬度的,不同的位置都有着特殊的含義。Bytecode 作為平台無關性的中間碼可以作為輸入材料運作在各種平台的虛拟機上,這就是 Java 當年最響亮的廣告語:Write Once and Run Anywhere (一次編寫到處運作)的來源。

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考

從上圖可以看出,一旦你生成了 .class 檔案,你就可以把他運作在各種平台上,并且它會被虛拟機轉換為原生機器碼。

Android VM

目前 Android 虛拟機有兩種類型:Dalvik 和 ART,他們都是用來運作 Android App 的。在移動端上,所有的資源都是受限制的例如,電池電量、 CPU 運算能力、記憶體資源。是以必須優化程式讓他能在低功率的裝置上運作。

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考

從上面的圖可以看出,除了最後兩步其餘都和 JVM 相同。差異在于在 Apk 編譯期會使用 Dex 編譯器優化 .class 檔案,生成 .dex 檔案作為 Android VM 的執行代碼源頭。

對比 JVM 和 Android VM,從架構的角度來看二者最大的差異在于 JVM 的指令是基于棧的,而 Android VM 的指令是基于寄存器的。最直覺的效果就是 Android VM 執行程式的效率更高,這是因為寄存器離 CPU 更近、資料傳輸更快。而且基于棧模型的 JVM 在執行時, CPU 需要處理更多的指令去搬運資料,很明顯 Android VM 在執行程式時需要的指令會更少,而且省去了存儲部分記憶體太小(對于 JVM 運作架構的更多内容請閱讀《深入了解 Java 虛拟機》第八章 - 虛拟機位元組碼執行引擎)

Android VM JVM
基于寄存器模式,運作時速度更快更節省記憶體 基于棧模式
使用自己的特有的位元組碼且輸入源為 .dex 檔案從 Android 2.2 開始 Dalvik 虛拟機引入了 JIT 及時編譯功能 執行位元組碼輸入源為 .class 檔案擁有 JIT 功能
每一個 Application 都有一個單獨的 VM 一個 JVM 執行個體可以共享個多個 Application
是 Android 平台特有的,不支援其他平台作業系統 JVM 支援多平台作業系統
每個 Application 都有常量池 每個 .class 都有常量池
可執行檔案是 apk 可執行檔案是 jar

為什麼 Android 自造虛拟機去替代 JVM?

為什麼 Android 放棄現成的 JVM 不用非得自己造虛拟機出來呢?原因有以下幾點。

  • Though JVM is free, it was under GPL license, which is not good for Android as most the Android is under Apache license.
  • JVM was designed for desktops and it is too heavy for embedded devices.
  • DVM takes less memory, runs and loads faster compared to JVM.
​ — The OHIO State University

翻譯過來是:

  • 盡管JVM是免費的,但它已獲得GPL許可,這對Android不利,因為大多數Android均已獲得Apache許可。
  • JVM是為桌上型電腦設計的,對于嵌入式裝置而言太重了。
  • 與JVM相比,DVM占用更少的記憶體,運作和加載速度更快。

​ — 俄亥俄州立大學

總結

Android VM 是基于優化過的 Bytecode 位元組碼檔案工作的,它針對移動平台優化。這是因為移動平台相比 PC 具有更少的記憶體、較低的功耗、效率更低的CPU。

程序間的記憶體配置設定

Android OS 使用記憶體分頁和記憶體映射(Memory-mapped )來管理記憶體。在 Android OS 運作時不會浪費任何記憶體空間,它會嘗試使用所有的記憶體。例如,==系統會在應有關閉後将其儲存在記憶體當中,以便使用者快速切換應用程式。==是以,通常情況下 Android OS 幾乎沒有空閑的記憶體。要在系統程序和許多應用直接正确配置設定記憶體,記憶體管理至關重要。

記憶體類型

Android 裝置包含是三種不同類型的記憶體:RAM、zRAM和存儲器(CPU 和 GPU 通路同一個 RAM)。

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考
  • RAM:RAM 就是我們常常聽到的手機記憶體,但通常容量有限,一般高端裝置擁有更大的 RAM。
  • zRAM:相當于 swap 區域是用于交換空間的 RAM 分區。Android 系統沒有使用磁盤作為交換空間,而是直接在 RAM 上開辟了一個單獨的區域。 這樣的好處就是更快,但弊端也很明顯:縮小了 RAM 的體積。所有資料在放入 zRAM 時都會被壓縮,在換出的時候在解壓,這是典型的時間換空間的方式。zRAM 擁有動态拓展功能,其空間大小會随着資料的換入換出而增大縮小。裝置制造商可以設定 zRAM 大小上限。
  • 存儲器可以類比磁盤,它包含了很多持久化存儲的資料,存儲器相比上面兩種類型的記憶體容量要大很多。在 Android 上,為了避免因頻繁寫入導緻的損壞、縮短存儲器的壽命,是以 Android 沒有像 Linxu 那樣用磁盤作為交換空間。

記憶體共享

為了在 RAM 中容納所需要的一切,Android 會嘗試跨程序共享 RAM 頁面。可以通過以下方式實作:

  • Zygote。系統啟動并加載通用架構代碼和資源時,Zygote 随之啟動。在 Android 中每一個程序都是通過 Zygote fork 而來,這樣新啟動的程序就擁有了系統資源的記憶體映射表,然後在新程序中加載并運作應用代碼。這種方法可以使為架構代碼和資源分的 RAM 在不同程序之間共享。
  • 大多數的靜态資料會映射到一個程序中。這種方式可以使資料在不同的程序中共享的同時,還能換入換出。靜态資料示例包括:Dalvik 代碼(通過将其放入預先連結的

    .odex

    檔案中進行直接記憶體映射)、應用資源(通過将資源表格設計為可記憶體映射的結構以及通過對齊 APK 的 zip 條目)和傳統項目元素(如

    .so

    檔案中的原生代碼)。
  • 在很多地方,Android 使用明确配置設定的共享記憶體區域(通過 ashmem 或 gralloc)在程序間共享同一動态 RAM。例如,視窗 surface 使用在應用和螢幕合成器之間共享的記憶體,而光标緩沖區則使用在内容提供器和用戶端之間共享的記憶體。

限制應用記憶體

為了維持多任務環境的正常運作,Android 會為每個應用的堆大小設定硬性上限。不同裝置的确切堆大小上限取決于裝置的總體可用 RAM 大小。如果您的應用在達到堆容量上限後嘗試配置設定更多記憶體,則可能會收到

OutOfMemoryError

在某些情況下,例如,為了确定在緩存中儲存多少資料比較安全,您可能需要查詢系統以确定目前裝置上确切可用的堆空間大小。您可以通過調用 ActivityManager#getMemoryClass() 向系統查詢此數值。此方法傳回一個整數,表示應用堆的可用兆位元組數。

記憶體頁面

同樣的 Android 也會将記憶體分頁管理,每個頁也是 4k 大小。系統會将頁面視為 「可用」和「已使用」兩種。可用頁面就是未使用的 RAM。已使用的頁面就是系統目前正在使用的 RAM,并分為以下類别:

  • 緩存頁面:在存儲器中有檔案對象的記憶體。
    • 私有頁面:某個程序獨占的
      • 幹淨頁面:映射為檔案系統中未經過修改的檔案副本;可以由

        kswapd

        删除釋放記憶體(因為檔案未經過修改,是以可以再次讀取到一模一樣的資料,删了也沒關系)
      • 髒頁面:存儲器中經過修改的檔案,可由

        kswapd

        移動到 zRAM 中增加記憶體空間
    • 貢獻頁面:多個程序之間共享的
      • 幹淨頁面:同上。
      • 髒頁面:存儲器中經過修改的檔案,通過

        kswapd

        或者通過其他方法将更改寫回到存儲器,咱家可用空間
  • 匿名頁面:存儲器中沒有對應檔案的記憶體。
    • 髒頁面:可由

      kswapd

      移動到 zRAM/在 zRAM 中進行壓縮以增加可用記憶體

記憶體不足的處理政策

Android 系統在面臨記憶體不足時有兩種解決政策:核心交換守護程序和低記憶體終止程序。

核心交換守護程序

核心交換守護程序 kswapd 是 Linux 核心的一部分,用于将已使用的記憶體轉換為可用記憶體。等裝置上的記憶體不足之處 kswapd 将變化為活躍狀态。Linux 核心對于可用記憶體設有上下門檻值,當可用記憶體下降到下限門檻值以下時,kswapd 開始回收記憶體。當可用記憶體大于上限門檻值的時候,kswapd 停止回收記憶體。

kswapd

可以删除幹淨頁來回收他們,因為這些頁受到存儲器的支援且未經過修改。如果某個程序需要使用已經删除的幹淨頁的時候,則系統會将該頁面從存儲器複制到 RAM。此操作稱為“請求分頁”。

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考

對于髒頁面

kswapd

可以将緩存的私有髒頁和匿名髒頁移動到 zRAM 進行壓縮。這樣可以釋放 RAM 中可用的記憶體。如果某個程序嘗試處理 zRAM 中的髒頁,該頁将被解壓縮并回到 RAM。如果與壓縮頁面關聯的記憶體被終止了,則該頁面會在 zRAM 中被删除。

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考

低記憶體終止守護程序

很多時候通過

kswapd

核心交換程序釋放出來的記憶體并不夠使用,這種情況下,系統會使用

onTrimMemeory()

來通知應用程序記憶體不足,開發者應該減少記憶體的使用量。如果這還不夠,核心則開始啟動低記憶體守護程序(LMK)殺程序。

LMK 使用一個名為

oom_abj_score

的評分機制來計算優先級,分數高的将優先被殺死。一般情況下,背景應用最先被殺系統程序最後被終止。下表列出了從高到低的 LMK 評分類别。評分最高的類别,即第一行中的項目将最先被終止:

「抄底 Android 記憶體優化 6」—— 淺析 Android 虛拟機記憶體管理JVM vs Android VM程式間的記憶體配置設定應用記憶體管理參考

應用記憶體管理

記憶體資源在手機裝置上是非常寶貴的,雖然 Android VM (ART、Dalivk)都有自動記憶體回收政策(GC),但是這并不意味着我們可以對記憶體置之不理,開發者仍然需要避免記憶體洩漏,并在适當的時候釋放對象引用(Reference)。

在裝置出廠的時候,不同的廠商會根據裝置情況制定單個應用的最大可記憶體限制,通過以下指令行檢視:

// dvm 最大可用堆記憶體:
adb shell getprop | grep dalvik.vm.heapsize
           
// 單個程式限制最大可用堆記憶體:
adb shell getprop | grep heapgrowthlimit
           

超過單個程式限制最大堆記憶體則會抛出OOM,如果設定了手機開啟了 largeHeap ,則可提高到 dvm 最大堆記憶體才OOM。

監控可用記憶體和記憶體使用量

目前 Android Stuido 的 profile 工具已經很強大,開發者可以記憶體性能剖析器直覺的查找和診斷記憶體問題。Android Studio 的官方文檔對此介紹已經非常詳盡,對此不再贅述,參見:記憶體性能剖析器。

釋放記憶體以響應事件

在上面的一節「記憶體不足的處理政策」中我們提到 Android 裝置在記憶體不足的時候會使用多種記憶體回收方式回收記憶體,或者殺死應用。為了能夠感覺到系統記憶體不足的情況并避免系統殺死應用程序,Android 提供了 ComponentCallbacks2 接口。此接口需要在 Activity 中實作,在 Activity 啟動後會将 callback 注冊進入,并在記憶體情況發生變化的時候回調 onTrimMemory(int level) 方法。如果要随機檢測目前 level 可以使用ActivityManager.getMyMemoryState(RunningAppProcessInfo方法。

import android.content.ComponentCallbacks2
    // Other import statements ...

    class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

        // Other activity code ...

        /**
         * Release memory when the UI becomes hidden or when system resources become low.
         * @param level the memory-related event that was raised.
         */
        override fun onTrimMemory(level: Int) {

            // Determine which lifecycle or system event was raised.
            when (level) {

                ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                    /*
                       Release any UI objects that currently hold memory.

                       The user interface has moved to the background.
                    */
                }

                ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
                ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
                ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                    /*
                       Release any memory that your app doesn't need to run.

                       The device is running low on memory while the app is running.
                       The event raised indicates the severity of the memory-related event.
                       If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                       begin killing background processes.
                    */
                }

                ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
                ComponentCallbacks2.TRIM_MEMORY_MODERATE,
                ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                    /*
                       Release as much memory as the process can.

                       The app is on the LRU list and the system is running low on memory.
                       The event raised indicates where the app sits within the LRU list.
                       If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                       the first to be terminated.
                    */
                }

                else -> {
                    /*
                      Release any non-critical data structures.

                      The app received an unrecognized memory level value
                      from the system. Treat this as a generic low-memory message.
                    */
                }
            }
        }
    }
           

檢視您應該使用多少記憶體

為了允許多個程序同時運作,Android 針對為每個應用配置設定的堆大小設定了硬性限制。裝置的确切堆大小限制因裝置總體可用的 RAM 多少而異。如果您的應用已達到堆容量上限并嘗試配置設定更多記憶體,系統就會抛出

OutOfMemoryError

為了避免用盡記憶體,您可以查詢系統以确定目前裝置上可用的堆空間。您可以通過調用

getMemoryInfo()

向系統查詢此數值。它将傳回一個

ActivityManager.MemoryInfo

對象,其中會提供與裝置目前的記憶體狀态有關的資訊,包括可用記憶體、總記憶體和記憶體門檻值(如果達到此記憶體級别,系統就會開始終止程序)。

ActivityManager.MemoryInfo

對象還會提供一個簡單的布爾值

lowMemory

,您可以根據此值确定裝置是否記憶體不足。

以下代碼段示例示範了如何在應用中使用

getMemoryInfo()

方法。

fun doSomethingMemoryIntensive() {

        // Before doing something that requires a lot of memory,
        // check to see whether the device is in a low memory state.
        if (!getAvailableMemory().lowMemory) {
            // Do memory intensive work ...
        }
    }

    // Get a MemoryInfo object for the device's current memory status.
    private fun getAvailableMemory(): ActivityManager.MemoryInfo {
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        return ActivityManager.MemoryInfo().also { memoryInfo ->
            activityManager.getMemoryInfo(memoryInfo)
        }
    }
    
           

參考

記憶體管理概覽

程序間的記憶體配置設定

管理應用記憶體

Android記憶體管理分析總結

使用Logcat寫入和檢視日志

調查 RAM 使用情況

《深入解析Android虛拟機》

《深入了解Android:Java虛拟機ART》