1. 程式人生 > >Android應用優化之記憶體概念

Android應用優化之記憶體概念

導語

現在的Android智慧手機發展資訊萬變,從一開始的HTC到小米價格戰到現在高階市場份額戰,在軟硬體都發生了翻天覆地的變化。在硬體上記憶體從一開始的一兩百M到現在4G。從軟體上我們從一開始為了實現需求而寫程式碼到現在為了程式碼更健壯、更漂亮而進行不斷優化程式碼。這些都是Android發展的必然一步。今天我來跟大家一起分享Android記憶體優化的相關概念和實踐。

概念

程序記憶體與RAM之間的關係

程序記憶體既是虛擬記憶體(或者叫邏輯記憶體),而程式的執行需要實實在在的記憶體,即實體記憶體(RAM),在需要的時候作業系統會將程式執行中申請的記憶體(虛擬記憶體)對映到RAM,讓程序能夠使用實體記憶體。

Android中的程序

Google提供的Android整體架構圖,可以看到Android系統是基於Linux核心的,但針對移動裝置較低的記憶體和能耗低的需求,Android按照自身需要開發低耗的元件和庫,但是Android程序
最明顯的記憶體特徵是與zygote共享記憶體。為了加快啟動速度及節約記憶體,Android應用的程序都是有zygote fork出來的。由於zygote已經載入了完整的Dalvik虛擬機器和Android應用框架的程式碼,fork出的程序和zygote共享同一塊記憶體,這樣就節約了每個程序單獨載入的時間和記憶體。
這裡寫圖片描述

虛擬記憶體分割槽

虛擬記憶體對各種型別的資料進行儲存,由於資料的雜亂,因此程式劃分五個區域分別管理不同的資料:程式暫存器(Program Count Register)、本地方法棧(Native Stack)、方法區(Methon Area)、棧(Stack)、堆(Heap)

Java虛擬機器、Dalvik虛擬機器、ART虛擬機器的區別

虛擬機器(Virtual Machine),這個名詞相信大家都不陌生。說到虛擬機器我們肯定要說到Dalvik和JVM。
DVM是Dalvik Virtual Machine的縮寫,是安卓虛擬機器的意思。(為什麼不叫AVM->Android Virtual Machine呢?原因是其作者以其祖上居住過的名為Dalvik的村子命名)。
JVM是相對Java Virtual Machine而言的

,對於Java(Oracle公司)與Android(Google公司)的關係大家都懂。

JVM執行的是.class位元組碼,DVM執行的是.dex位元組碼格式。
Java類被編譯成一個或多個位元組碼.class檔案,打包到.jar檔案中,java虛擬機器從相應的.class檔案和.jar檔案中獲取相應的位元組碼。
java類被編譯成.class檔案後,會通過一個dx工具將所有的.class檔案轉換成一個.dex檔案,然後DVM會從其中讀取指令和資料。

JVM基於棧,DVM基於暫存器。
JVM基於棧結構,程式在執行時虛擬機器需要頻繁的從棧上讀取寫入資料,這個過程需要更多的指令分派與記憶體訪問次數,很耗費CPU時間。
DVM基於暫存器架構,資料的訪問通過暫存器間直接傳遞,這樣的訪問方式比基於棧方式要快很多。

ART完整名稱是Android Runtime。在Android5.0中,ART取代了Dalvik虛擬機器(安卓在4.4中釋出了ART)。
ART之所以會比Dalvik快,是因為ART執行的是本地機器指令,而Dalvik執行的是Dex位元組碼,通過通過直譯器執行。儘管Dalvik也會對頻繁執行的程式碼進行JIT生成本地機器指令來執行,但畢竟在應用程式執行的過程中將Dex位元組碼翻譯成本地機器機器指令也會影響到應用程式本身的執行,因此即使Dalvik使用了JIT,也在一定程度上也比不上直接就可以執行本地機器指令的執行時。

Android記憶體分配機制

Android裝置出廠以後,虛擬機器對單個應用的最大記憶體分配就確定下來了,如dalvik.vm.heapstartsize=8m,超出這個值就會OOM。而Android為每個程序分配記憶體的時候,採用了彈性的分配方式,也就是剛開始並不會一下分配很多記憶體給每個程序,而是給每一個程序分配一個“夠用”的量。這個量是根據每一個裝置實際的實體記憶體大小來決定的。隨著應用的執行,可能會發現當前的記憶體可能不夠使用了,這時候Android又會為每個程序分配一些額外的記憶體大小。但是這些額外的大小並不是隨意的,也是有限度的,系統不可能為每一個App分配無限大小的內除。對於這個屬性值是定義在/system/build.prop檔案中,它配置dalvik堆的有關設定。具體設定由如下三個屬性來控制

  • dalvik.vm.heapstartsize
    堆分配的初始大小,調整這個值會影響到應用的流暢性和整體ram消耗。這個值越小,系統ram消耗越慢,但是由於初始值較小,一些較大的應用需要擴張這個堆,從而引發gc和堆調整的策略,會應用反應更慢。相反,這個值越大系統ram消耗越快,但是程式更流暢。
  • dalvik.vm.heapgrowthlimit
    受控情況下的極限堆(僅僅針對dalvik堆,不包括native堆)大小,dvm heap是可增長的,但是正常情況下dvm heap的大小是不會超過dalvik.vm.heapgrowthlimit的值。這個值控制那些受控應用的極限堆大小,如果受控的應用dvm heap size超過該值,則將引發oom。
  • dalvik.vm.heapsize
    不受控情況下的極限堆大小,這個就是堆的最大值。不管它是不是受控的。這個值會影響非受控應用的dalvikheap size。一旦dalvik heap size超過這個值,直接引發oom。

       用他們三者之間的關係做一個簡單的比喻:分配dalvik heap就好像去食堂打飯,有人飯量大,要吃三碗,有人飯量小,連一碗都吃不完。如果食堂按照三碗的標準來給每個人打飯,那絕對是鋪張浪費,所以食堂的策略就是先打一碗,湊合吃,不夠了自己再來加,設定堆大小也是一樣,先給一個合理值,湊合用,自己不夠了再跟系統要。食堂畢竟是做買賣的,如果很多人明顯吃不了那麼多,硬是一碗接著一碗。為了制止這種不合理的現象,食堂又定了一個策略,一般人就只能吃三碗。但是如果虎背熊腰的大漢確實有需要,可以吃上五碗,超過五碗就不給了(太虧本了)。

情景 值含義
開始給一碗 dalvik.vm.heapstartsize
一般人最多吃三碗 dalvik.vm.heapgrowthlimit
虎背熊腰的大漢最多能吃五碗 dalvik.vm.heapsize

在android開發中,如果要使用大堆。需要在manifest中指定android:largeHeap為true。這樣dvm heap最大可達dalvik.vm.heapsize。

RAM不足會出現的現象

當RAM不足時,Android程式主要會出現以下三種情形。

  • 記憶體洩漏

    不止Android程式設計師,記憶體洩露應該是大部分程式設計師都遇到過的問題,可以說大部分的記憶體問題都是記憶體洩露導致的,有興趣的同學可以跳到我的另外連結來了解。

  • 記憶體抖動

    Android裡記憶體抖動是指記憶體頻繁地分配和回收,而頻繁的gc會導致卡頓,嚴重時還會導致OOM。
    這裡寫圖片描述

  • 記憶體溢位

    記憶體溢位是一種非常嚴重的後果,大家可以根據接下來的分析去判斷自己的程式有沒做對於的優化事項。

Android應對RAM不足——記憶體釋放垃圾回收機制

  • Application Framework

    Anroid基於程序中執行的元件及其狀態規定了預設的五個回收優先順序:

    空程序:正常情況下,為了平衡系統整體效能,Android不儲存這些程序
    後臺程序:存放於一個LRU快取列表中,先殺死處於列表尾部的程序
    服務程序:正常不會被殺死
    可見程序:正常不會被殺死
    前臺程序:正常不會被殺死

    Android為每一個程序分配了優先順序的概念,系統需要進行記憶體回收時最先回收空程序,然後是後臺程序,以此類推最後才會回收前臺程序。

  • Dalvik 虛擬機器

    上面我們說到當Android裝置出廠以後,虛擬機器對單個應用的最大記憶體分配就確定下來了,超出這個值就會OOM。

    Android的GC操作,GC全稱是Garbage Collection,也就是所謂的垃圾回收。Android系統會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的物件進行回收。
    這裡寫圖片描述

上圖藍色圈圈表示是記憶體中的物件,圈圈之間的箭頭是物件的引用,上面的物件有的在使用,有的已經不再使用,那GC操作會從一個叫作Roots的物件開始檢查。
這裡寫圖片描述

可以看出,黃色的物件仍然會被繼續保留使用,而藍色的物件就會在GC操作當中被系統回收掉了,這就是一次簡單的垃圾回收。

虛擬機器通過可達性(Reachability)來判斷物件是否存活,基本思想:以”GC Roots”的物件作為起始點向下搜尋,搜尋形成的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連(即不可達的),則該物件被判定為可以被回收的物件,反之不能被回收。

另一方面,上文提及到Android系統會在適當的時機觸發GC操作,由於Java語言的特性,不像C++等語言需要人為地進行垃圾回收,還有我們也不需要主動去通知系統進行垃圾回收。當系統進行垃圾回收,會在列印臺進行列印。列印的資料主要分為四個部分:

  1. GC_Reason,這個是觸發這次GC操作的原因,一般情況下有以下幾種原因:

    • GC_CONCURRENT: 當我們應用程式的堆記憶體快要滿的時候,系統會自動觸發GC操作來釋放記憶體。
    • GC_FOR_MALLOC: 當我們的應用程式需要分配更多記憶體,可是現有記憶體已經不足的時候,系統會進行GC操作來釋放記憶體。
    • GC_HPROF_DUMP_HEAP: 當生成HPROF檔案的時候,系統會進行GC操作,關於HPROF檔案我們下面會講到。
    • GC_EXPLICIT: 這種情況就是我們剛才提到過的,主動通知系統去進行GC操作,比如呼叫System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的。
  2. Amount_freed,表示系統通過這次GC操作釋放了多少記憶體。

  3. Heap_stats中會顯示當前記憶體的空閒比例以及使用情況(活動物件所佔記憶體 / 當前程式總記憶體)。
  4. Pause_time表示這次GC操作導致應用程式暫停的時間。

下面是一次GC操作在LogCat中列印的日誌:
這裡寫圖片描述

深入分析垃圾回收時,我們需要知道Android Dalvik Heap與原生Java一樣,將堆的記憶體空間分為三個區域,Young Generation(年輕代)、Old Generation(年老代)、Permanent(讀音:[ˈpɜ:rmənənt]) Generation(永久代)

最近分配的物件會存放在Young Generation區域,當這個物件在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後累積一定時間再移動到Permanent Generation區域。系統會根據記憶體中不同的記憶體資料型別分別執行不同的gc操作。

回收演算法

  • 標記回收演算法(Mark and Sweep GC)

從”GC Roots”集合開始,將記憶體整個遍歷一次,保留所有可以被GC Roots直接或間接引用到的物件,而剩下的物件都當作垃圾對待並回收,這個演算法需要中斷程序內其它元件的執行並且可能產生記憶體碎片。

  • 複製演算法 (Copying)

將現有的記憶體空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的記憶體中的存活物件複製到未被使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。

  • 標記-壓縮演算法 (Mark-Compact)

先需要從根節點開始對所有可達物件做一次標記,但之後,它並不簡單地清理未標記的物件,而是將所有的存活物件壓縮到記憶體的一端。之後,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,因此,其價效比比較高。

  • 分代回收

將所有的新建物件都放入稱為年輕代的記憶體區域,年輕代的特點是物件會很快回收,因此,在年輕代就選擇效率較高的複製演算法。當一個物件經過幾次回收後依然存活,物件就會被放入稱為老生代的記憶體空間。對於新生代適用於複製演算法,而對於老年代則採取標記-壓縮演算法。
這裡寫圖片描述

然而複製演算法和標記-壓縮演算法的區別在於前者是用空間換時間後者則是用時間換空間。

前者的在工作的時候是不沒有獨立的“mark”與“copy”階段的,而是合在一起做一個動作,就叫scavenge(或evacuate,或者就叫copy)。也就是說,每發現一個這次收集中尚未訪問過的活物件就直接copy到新地方,同時設定forwarding pointer。這樣的工作方式就需要多一份空間。

後者在工作的時候則需要分別的mark與compact階段,mark階段用來發現並標記所有活的物件,然後compact階段才移動物件來達到compact的目的。如果compact方式是sliding compaction,則在mark之後就可以按順序一個個物件“滑動”到空間的某一側。因為已經先遍歷了整個空間裡的物件圖,知道所有的活物件了,所以移動的時候就可以在同一個空間內而不需要多一份空間。

結合上面我們說的ART與DVM的比較,在【Android】揭祕 ART 細節 —- Garbage collection文章中圖文結合,十分形象地描述了ART的GC優點,大家可以自行去閱讀,本文就不重複篇幅去描述了。

不要頻繁的引發GC,執行GC操作的時候,任何執行緒的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續執行, 故而如果程式頻繁GC, 自然會導致介面卡頓。

Android記憶體分析

上文我們介紹了很多關於記憶體的概念知識,在我們瞭解相關後,我們通過檢測工具來發現問題.本文先介紹最常見的三款檢測工具.

  • Android Monitor Memory

    在我們使用Android Studio時,我們可以通過Minitor Memory來觀察記憶體的使用情況.
    這裡寫圖片描述

    圖中頂部選擇不同的裝置,和對應除錯的程序.
    圖中水平方向是時間軸,豎直方向是記憶體的分配情況.深藍色區域表示使用中的記憶體總量,淺灰色表示未作分配的記憶體.
    記憶體分析工具,從左到右四個按鈕,依次是:

    1. Enabled/disenable 檢測開關,不用管.
    2. Initiate GC 手動呼叫GC.在我們抓取記憶體前,先手動點選一下這個按鈕來手動觸發GC行為.
    3. Dump Java Heap 點選生成一個檔案(包名+日期+“.hprof”).記錄的是一個時間段內,程式使用記憶體的情況.
    4. start Allocation Tracking 開始分配追蹤,第一次點選可以指定追蹤記憶體的開始位置,第二次點選可以結束追蹤的位置。這樣我們截取了一段要分析的記憶體,等待幾秒鐘Android Studio會給我們開啟一個Allocation檢視

瞭解完基本操作我們使用執行一下專案,然後我們手動操作GC,再一定量的操作後,看看專案的記憶體使用情況,此時含有正常新增的記憶體使用,也可能出現了記憶體洩漏的情況.我們點選Dump Java Heap,等一小會兒會自動生成.hprof檔案並自動彈出HPROF Viewer來分析記憶體使用情況.
這裡寫圖片描述

我們看著上圖來分析,顯示左上角的顯示方式,有兩個選項分別是Heap區域和Class List View的展示方式.
Heap型別有:
App Heap – 當前App使用的Heap
Image Heap – 磁碟上當前App的記憶體對映拷貝
Zygote Heap – Zygote程序Heap

Class List View有:
Class List View – 類列表方式
Package Tree View – 根據包結構的樹狀顯示

HPROF Viewer主要分三大模組
模組a:這個應用中所有類的名字
模組b:左邊類的所有例項
模組c:在選擇B中的例項後,這個例項的引用樹

模組a名詞解析:

名詞 解析
Class Name 類名,Heap中的所有Class
Total Count 記憶體中該類這個物件總共的數量,有的在棧中,有的在堆中
Heap Count 堆記憶體中這個類 物件的個數
Sizeof 每個該例項佔用的記憶體大小
Shallow Size 所有該類的例項佔用的記憶體大小
Retained Size 所有該類物件被釋放掉,會釋放多少記憶體

模組b名詞解析:

名詞 解析
Instance 該類的例項
Depth 深度, 從任一GC Root點到該例項的最短跳數
Dominating Size 該例項可支配的記憶體大小

b模組右上角有個AnalyzerTasks的按鈕, 點選會進入HPROF Analyzer的hprof的分析介面,點選Analyzer Tasks右邊的綠色執行箭頭,Android Studio會自動的根據此hprof檔案分析有哪些類是有記憶體洩漏的,如下圖所示:

這裡寫圖片描述

在Analyzer Tasks中的AnalyzerResults中看到分析洩漏的activities,在程式碼中例項只有一個,但我們在a模組來看 total count的值為2,heap count的值也為2,說明有一個是多餘的。我們在b模組看到了對應的兩個例項.根據分析我們可以在對應的程式碼就是修復.

但是遇到一個常見的問題,我們知道了這中記憶體洩漏的問題,但是我找到究竟是哪裡出現問題呀,所以接著的功能是Allocation Tracker,用來記憶體分配追蹤。在記憶體圖中點選途中標紅的部分,啟動追蹤,再次點選就是停止追蹤,隨後自動生成一個alloc結尾的檔案,這個檔案就記錄了這次追蹤到的所有資料,然後會在右上角開啟一個數據面板Allocation Tracker啟動追蹤
這裡寫圖片描述

Allocation Tracker檢視方式
有兩種檢視方式:
1. Group by Method:用方法來分類我們的記憶體分配
2. Group by Allocator:用記憶體分配器來分類我們的記憶體分配
這裡寫圖片描述

Group by Allocator方式.右擊直接跳到對應的程式碼.
點選統計圖按鈕,會生成上圖,扇形統計圖是以圓心為起點,最外層是其記憶體實際分配的物件,每一個同心圓可能被分割成多個部分,代表了其不同的子孫,每一個同心圓代表他的一個後代,每個分割的部分代表了某一帶人有多人,你雙擊某個同心圓中某個分割的部分,會變成以你點選的那一代為圓心再向外展開。

  • MAT

    接下來我們說一下比Memory Monitor更強大的MAT。MAT全稱 Eclipse Memory Analysis Tools 是一個分析Java堆資料的專業工具。

    首先整合MAT工具,我們可以在官網下載,地址:https://www.eclipse.org/mat/.如果你電腦有eclipse就直接安裝MAT外掛(不懂如何安裝的同學自行搜尋”eclipse安裝MAT外掛”).
    當我們集成了MAT工具後,我們回到AS IDE中,之前我們在分析Monitor Memory時候生成過.hprof檔案,這時需要通過AS轉換一下格式或者使用jdk自帶命令轉換,如下圖AS轉換:
    這裡寫圖片描述

    接著我們用MAT開啟轉換後的.hprof檔案.如下圖所示,我們要關注紅框內的功能.
    這裡寫圖片描述

    Shallow Heap :一個物件記憶體的消耗大小,不包含對其他物件的引用;
    Retained Heap :是shallow Heap的總和,也就是該物件被GC之後所能回收的記憶體大小;
    詳細解釋可參考文章:Shallow heap & Retained heap

    Histogram
    可列出每一個類的例項數。支援正則表示式查詢,也可以計算出該類所有物件的retained size
    這裡寫圖片描述

    Dominator Tree
    Dominator Tree:物件之間dominator關係樹。如果從GC Root到達Y的的所有path都經過X,那麼我們稱X dominates Y,或者X是Y的Dominator Dominator Tree由系統中複雜的物件圖計算而來。從MAT的dominator tree中可以看到佔用記憶體最大的物件以及每個物件的dominator。
    我們也可以右鍵選擇Immediate Dominator”來檢視某個物件的dominator。

Path to GC Roots
檢視一個物件到RC Roots的引用鏈
通常在排查記憶體洩漏的時候,我們會選擇exclude all phantom/weak/soft etc.references,
意思是檢視排除虛引用/弱引用/軟引用等的引用鏈,因為被虛引用/弱引用/軟引用的物件可以直接被GC給回收,我們要看的就是某個物件否還存在Strong 引用鏈(在匯出HeapDump之前要手動出發GC來保證),如果有,則說明存在記憶體洩漏,然後再去排查具體引用。(記得看最上方的箭頭,用包名來檢視)
這裡寫圖片描述

右擊檢視當前Object所有引用,被引用的物件:

List objects with (以Dominator Tree的方式檢視)

incoming references 引用到該物件的物件
outcoming references 被該物件引用的物件

Show objects by class (以class的方式檢視)

by incoming references 引用到該物件的物件
by outcoming references 被該物件引用的物件

Heap Dump Overview
這裡寫圖片描述

從工具欄中點開 Heap Dump Overview檢視,可以看到一個全域性的記憶體佔用資訊

OQL(Object Query Language)
類似SQL查詢語言
Classes:Table
Objects:Rows
Fileds: Cols

select * from com.example.mat.Listener

查詢size=0並且未使用過的ArrayList
select * from java.util.ArrayList where size=0 and modCount=0

查詢所有的Activity
select * from instanceof android.app.Activity

按紅色感嘆號查詢結果.

上面我們介紹了MAT幾個重要的功能點.下面我們開始來找出造成記憶體洩漏的凶手.

1.我們通過Dominator Tree檢視用Path to GC Roots方式來查詢.

這裡寫圖片描述
2.Object Query Language快速查詢

這裡寫圖片描述
不要認為輸命令很麻煩,但是比上面Path to GC Roots方式一個個包看的速度要快要準.

3.記憶體快照對比

這裡寫圖片描述
開啟兩個時間點的.hprof檔案,都打開了Histogram標籤,然後按右上角Compare to anther Heap Dump按鈕,進行對比.

小結:我們使用MAT的最終目的就是能順利地找出記憶體洩漏的地方我們上面介紹了三種方式,之前我們通過Monitor Memory的方式已經大概能定位洩漏類,然後我們使用MAT前兩個方式十分直接地去查詢究竟是哪個物件引用出現了洩漏問題.而第三個對比方式,在我們還是不確定位置情況下可以使用,我們可以通過對比的方式來判斷洩漏的位置.

  • LeakCanary

這裡寫圖片描述
LeakCanary是square公司出品。有一句話為square出品,必屬精品。
專案地址:https://github.com/square/leakcanary。它的工作原理、接入流程、使用方法我就不在這裡一一介紹了,網上關於LeakCanary的教程實在太多。它是你開發必備工具。這就是我對它的理解。

總結:
整篇文章我們可以分為兩部分,第一部分是對記憶體基礎知識的概述.大家需要注意結構寫法,是按照實體記憶體、虛擬機器記憶體、Androi記憶體分配機制、Android記憶體回收機制。針對異常導致的記憶體洩漏、記憶體抖動、記憶體溢位我們利用Monitor Memory,MAT,LeakCanary對記憶體進行了一系列的分析 其中Monitor Memory自帶的記憶體分析工具直觀方便,但其功能卻不如MAT強大,特別是沒有有效的搜尋、排序等功能。遇到一些棘手的問題,可能還是要藉助MAT來分析記憶體。大家覺得我的描述不夠詳細或準確,可以根據我這個文章描述結構形成一個基本的認識後,再根據每一個小點進行細化學習。
說說我自己的實操方法,我認為大型專案都要引用LeakCanary包。在debug環境下,如果出現了記憶體洩漏,我們會馬上收到提示,我們可以根據LeakCanary的資訊收集地方對洩漏做一開始的判斷,如果有相當的經驗後,我們可以直接修改,如果不能我們可以通過Monitor Memory來更清晰的定位,遇到更加棘手的問題,我們再使用MAT的方式,根據它豐富的工具一步步來定位記憶體洩漏的物件,並對其分析解決。

不過分析了這麼多,最重要的是實操。小夥伴們馬上實踐起來。