如今的Android智能手機發展信息萬變,從一開始的HTC到小米價格戰到如今高端市場份額戰,在軟硬件都發生了翻天覆地的變化。在硬件上內存從一開始的一兩百M到如今4G。從軟件上咱們從一開始爲了實現需求而寫代碼到如今爲了代碼更健壯、更漂亮而進行不斷優化代碼。這些都是Android發展的必然一步。今天我來跟你們一塊兒分享Android內存優化的相關概念和實踐。html
進程內存與RAM之間的關係java
進程內存既是虛擬內存(或者叫邏輯內存),而程序的運行須要實實在在的內存,即物理內存(RAM),在須要的時候操做系統會將程序運行中申請的內存(虛擬內存)映射到RAM,讓進程可以使用物理內存。android
Android中的進程git
Google提供的Android總體架構圖,能夠看到Android系統是基於Linux內核的,但針對移動設備較低的內存和能耗低的需求,Android按照自身須要開發低耗的組件和庫,可是Android進程
最明顯的內存特徵是與zygote共享內存。爲了加快啓動速度及節約內存,Android應用的進程都是有zygote fork出來的。因爲zygote已經載入了完整的Dalvik虛擬機和Android應用框架的代碼,fork出的進程和zygote共享同一塊內存,這樣就節約了每一個進程單獨載入的時間和內存。程序員
虛擬內存分區github
虛擬內存對各類類型的數據進行存儲,因爲數據的雜亂,所以程序劃分五個區域分別管理不一樣的數據:程序寄存器(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 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++等語言須要人爲地進行垃圾回收,還有咱們也不須要主動去通知系統進行垃圾回收。當系統進行垃圾回收,會在打印臺進行打印。打印的數據主要分爲四個部分:
GC_Reason,這個是觸發此次GC操做的緣由,通常狀況下有如下幾種緣由:
Amount_freed,表示系統經過此次GC操做釋放了多少內存。
下面是一次GC操做在LogCat中打印的日誌:
深刻分析垃圾回收時,咱們須要知道Android Dalvik Heap與原生Java同樣,將堆的內存空間分爲三個區域,Young Generation(年輕代)、Old Generation(年老代)、Permanent(讀音:[ˈpɜ:rmənənt]) Generation(永久代)
最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到必定程度,它會被移動到Old Generation,最後累積必定時間再移動到Permanent Generation區域。系統會根據內存中不一樣的內存數據類型分別執行不一樣的gc操做。
從」GC Roots」集合開始,將內存整個遍歷一次,保留全部能夠被GC Roots直接或間接引用到的對象,而剩下的對象都看成垃圾對待並回收,這個算法須要中斷進程內其它組件的執行而且可能產生內存碎片。
將現有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。
先須要從根節點開始對全部可達對象作一次標記,但以後,它並不簡單地清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。以後,清理邊界外全部的空間。這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。
將全部的新建對象都放入稱爲年輕代的內存區域,年輕代的特色是對象會很快回收,所以,在年輕代就選擇效率較高的複製算法。當一個對象通過幾回回收後依然存活,對象就會被放入稱爲老生代的內存空間。對於新生代適用於複製算法,而對於老年代則採起標記-壓縮算法。
然而複製算法和標記-壓縮算法的區別在於前者是用空間換時間後者則是用時間換空間。
前者的在工做的時候是不沒有獨立的「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 Monitor Memory
在咱們使用Android Studio時,咱們能夠經過Minitor Memory來觀察內存的使用狀況.
瞭解完基本操做咱們使用運行一下項目,而後咱們手動操做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文件分析有哪些類是有內存泄漏的,以下圖所示:
可是遇到一個常見的問題,咱們知道了這中內存泄漏的問題,可是我找到到底是哪裏出現問題呀,因此接着的功能是Allocation Tracker,用來內存分配追蹤。在內存圖中點擊途中標紅的部分,啓動追蹤,再次點擊就是中止追蹤,隨後自動生成一個alloc結尾的文件,這個文件就記錄了此次追蹤到的全部數據,而後會在右上角打開一個數據面板Allocation Tracker啓動追蹤.
Allocation Tracker查看方式
有兩種查看方式:
Group by Allocator方式.右擊直接跳到對應的代碼.
點擊統計圖按鈕,會生成上圖,扇形統計圖是以圓心爲起點,最外層是其內存實際分配的對象,每個同心圓可能被分割成多個部分,表明了其不一樣的子孫,每個同心圓表明他的一個後代,每一個分割的部分表明了某一帶人有多人,你雙擊某個同心圓中某個分割的部分,會變成以你點擊的那一代爲圓心再向外展開。
MAT
接下來咱們說一下比Memory Monitor更強大的MAT。MAT全稱 Eclipse Memory Analysis Tools 是一個分析Java堆數據的專業工具。
首先集成MAT工具,咱們能夠在官網下載,地址: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快速查找
3.內存快照對比
小結:咱們使用MAT的最終目的就是能順利地找出內存泄漏的地方咱們上面介紹了三種方式,以前咱們經過Monitor Memory的方式已經大概能定位泄漏類,而後咱們使用MAT前兩個方式十分直接地去查找到底是哪一個對象引用出現了泄漏問題.而第三個對比方式,在咱們仍是不肯定位置狀況下可使用,咱們能夠經過對比的方式來判斷泄漏的位置.
總結:整篇文章咱們能夠分爲兩部分,第一部分是對內存基礎知識的概述.你們須要注意結構寫法,是按照物理內存、虛擬機內存、Androi內存分配機制、Android內存回收機制。針對異常致使的內存泄漏、內存抖動、內存溢出咱們利用Monitor Memory,MAT,LeakCanary對內存進行了一系列的分析 其中Monitor Memory自帶的內存分析工具直觀方便,但其功能卻不如MAT強大,特別是沒有有效的搜索、排序等功能。遇到一些棘手的問題,可能仍是要藉助MAT來分析內存。你們以爲個人描述不夠詳細或準確,能夠根據我這個文章描述結構造成一個基本的認識後,再根據每個小點進行細化學習。說說我本身的實操方法,我認爲大型項目都要引用LeakCanary包。在debug環境下,若是出現了內存泄漏,咱們會立刻收到提示,咱們能夠根據LeakCanary的信息收集地方對泄漏作一開始的判斷,若是有至關的經驗後,咱們能夠直接修改,若是不能咱們能夠經過Monitor Memory來更清晰的定位,遇到更加棘手的問題,咱們再使用MAT的方式,根據它豐富的工具一步步來定位內存泄漏的對象,並對其分析解決。不過度析了這麼多,最重要的是實操。小夥伴們立刻實踐起來。