在Android的性能優化的各個部分裏,內存的問題絕對是最使人頭疼的一部分,雖然Android有垃圾自動回收機制不須要手動干預,但也恰由於此,出現內存問題如內存泄漏和內存溢出等,若是對內存管理機制不熟悉,會更加難以排查問題。javascript
由於內存方面的知識較多且不易理解,內存優化部分就分兩篇文章進行,本文主要是關於Java、Android的內存分配、回收、GC等理論知識。html
談Android的內存,就不能不提Java的內存管理。Java程序在運行的過程當中會將其管理的內存分爲若干個不一樣的數據區:java
方法區:方法區存放的是類信息、常量、靜態變量,全部線程共享區域。android
虛擬機棧:每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,線程私有區域。算法
本地方法棧:與虛擬機棧相似,區別是虛擬機棧爲虛擬機執行Java方法服務,本地方法棧爲虛擬機使用到的Native方法服務。shell
堆:JVM管理的內存中最大的一塊,全部線程共享;用來存放對象實例,幾乎全部的對象實例都在堆上分配內存;此區域也是垃圾回收器(Garbage Collection)主要的做用區域,內存泄漏就發生在這個區域。緩存
程序計數器:可看作是當前線程所執行的字節碼的行號指示器;若是線程在執行Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令地址;若是執行的是Native方法,這個計數器的值爲空(Undefined)。性能優化
備註:
有一種習慣說法:把Java的內存區域分爲堆內存(Heap)和棧內存(Stack),Stack訪問快,Heap訪問慢,Stack中保存的是對象的引用(指針),Heap中保存的是對象的實例。微信
實際上這種說法是籠統、粗糙的,此處所說的Stack僅僅是虛擬機棧中的局部變量表部分。虛擬機棧與JVM運行時數據區涵蓋的都比此種說法多。併發
最基礎的收集算法:分爲「標記」和「清除」兩個階段,首先,標記出全部須要回收的對象,而後統一回收全部被標記的對象。
這種方法有兩個不足點:
- 效率問題,標記和清除兩個過程的效率都不高;
- 空間問題,標記清除以後會產生大量的不連續的內存碎片。
將內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊內存將用完了,就將還存活着的對象複製到另外一塊內存上面,而後再把已使用過的內存空間一次清理掉。
這種方法的特色:
- 優勢:實現簡單,運行高效;每次都是對整個半區進行內存回收,內存分配時也不須要考慮內存碎片等狀況,只要移動堆頂指針,按順序分配內存便可;
- 缺點:粗暴的將內存縮小爲原來的一半,代價實在有點高。
先標記須要回收的對象(標記過程與「標記-清除」算法同樣),而後把全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
這種方法的特色:
- 避免了內存碎片;
- 避免了「複製」算法50%的空間浪費;
- 主要針對對象存活率高的老年代。
根據對象的存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。
給對象中添加一個引用計數器,每當有一個地方引用該對象時,計數器值加1;引用失效時,計數器值減1;任意時刻計數器爲0的對象就是不可能再被使用的,表示該對象不存在引用關係。
這種方法的特色:
- 優勢:實現簡單,斷定效率也很高;
- 缺點:難以解決對象之間相互循環引用致使計數器值不等於0的問題。
以一系列成爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達),則證實此對象是不可用的。
Android系統的ART和Dalvik虛擬機扮演了常規的內存垃圾自動回收的角色, 使用paging 和 memory-mapping來管理內存,這意味着無論是由於建立對象仍是使用使用內存頁面形成的任何被修改的內存,都會一直存在於內存中,App惟一釋放內存的方法就是釋放App持有的對象引用,使GC能夠回收。
5.1內存回收
在Android的高級系統版本里面針對Heap空間有一個Generational Heap Memory的模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到必定程度,它會被移動到Old Generation,最後累積必定時間再移動到Permanent Generation區域。系統會根據內存中不一樣的內存數據類型分別執行不一樣的gc操做。例如,剛分配到Young Generation區域的對象一般更容易被銷燬回收,同時在Young Generation區域的gc操做速度會比Old Generation區域的gc操做速度更快。
5.2共享內存
- Android應用的進程都是從一個叫作Zygote的進程fork出來的。Zygote進程在系統啓動而且載入通用的framework的代碼與資源以後開始啓動。爲了啓動一個新的程序進程,系統會fork Zygote進程生成一個新的進程,而後在新的進程中加載並運行應用程序的代碼。這使得大多數的RAM pages被用來分配給framework的代碼,同時使得RAM資源可以在應用的全部進程之間進行共享。
- 大多數static的數據被mmapped到一個進程中。這不只僅使得一樣的數據可以在進程間進行共享,並且使得它可以在須要的時候被paged out。常見的static數據包括Dalvik Code,app resources,so文件等。
- 大多數狀況下,Android經過顯式的分配共享內存區域(例如ashmem或者gralloc)來實現動態RAM區域可以在不一樣進程之間進行共享的機制。例如,Window Surface在App與Screen Compositor之間使用共享的內存,Cursor Buffers在Content Provider與Clients之間共享內存。
5.3分配與回收內存
- 每個進程的Dalvik heap都反映了使用內存的佔用範圍。這就是一般邏輯意義上提到的Dalvik Heap Size,它能夠隨着須要進行增加,可是增加行爲會有一個系統爲它設定的上限。
- 邏輯上講的Heap Size和實際物理意義上使用的內存大小是不對等的,Proportional Set Size(PSS)記錄了應用程序自身佔用以及和其餘進程進行共享的內存。
5.4限制應用的內存
- 爲了整個Android系統的內存控制須要,Android系統爲每個應用程序都設置了一個硬性的Dalvik Heap Size最大限制閾值,這個閾值在不一樣的設備上會由於RAM大小不一樣而各有差別。若是你的應用佔用內存空間已經接近這個閾值,此時再嘗試分配內存的話,很容易引發OutOfMemoryError的錯誤。
- ActivityManager.getMemoryClass()能夠用來查詢當前應用的Heap Size閾值,這個方法會返回一個整數,代表你的應用的Heap Size閾值是多少Mb(megabates)。
5.5應用切換
- Android系統並不會在用戶切換應用的時候作交換內存的操做。Android會把那些不包含Foreground組件的應用進程放到LRU Cache中。例如,當用戶開始啓動了一個應用,系統會爲它建立了一個進程,可是當用戶離開這個應用,此進程並不會當即被銷燬,而是會被放到系統的Cache當中,若是用戶後來再切換回到這個應用,此進程就可以被立刻完整的恢復,從而實現應用的快速切換。
- 若是你的應用中有一個被緩存的進程,這個進程會佔用必定的內存空間,它會對系統的總體性能有影響。所以當系統開始進入Low Memory的狀態時,它會由系統根據LRU的規則與應用的優先級,內存佔用狀況以及其餘因素的影響綜合評估以後決定是否被殺掉。
須要特別注意的:
由上文咱們知道,GC操做主要是由系統決定的,可是咱們能夠監聽系統的GC過程,以此來分析咱們應用程序當前的內存狀態。
Dalvik虛擬機,每一次GC打印內容格式:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>複製代碼
含義解析
Art虛擬機,每一次GC打印內容格式:
I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>複製代碼
基本狀況和Dalvik沒有什麼差異,GC的Reason更多了,還多了一個LOS_Space_Status.
LOS_Space_Status:Large Object Space,大對象佔用的空間,這部份內存並非分配在堆上的,但仍屬於應用程序內存空間,主要用來管理 Bitmap 等佔內存大的對象,避免因分配大內存致使堆頻繁 GC。
經過命令行adb shell dumpsys meminfo packagename查看內存詳細佔用狀況:
其中幾個關鍵的數據:參考:
歡迎關注微信公衆號:按期分享Java、Android乾貨!