標籤:ini viewer efi monitor lips 基本類型 生命週期 git pat
1.記憶體的分配策略概述
程式運行時的記憶體配置有三種策略,分別是靜態,棧式的,和堆式的,對應的,三種儲存策略使用的記憶體空間主要分別是靜態儲存區(也稱方法區)、堆區和棧區。
靜態儲存區(方法區):記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式整個運行期間都存在。它主要存放待用資料、全域static資料和常量。
棧區:在執行函數時,函數內局部變數的儲存單元都可以在棧上建立,函數執行結束時這些儲存單元自動被釋放。棧記憶體配置運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
堆區:亦稱動態記憶體分配。程式在啟動並執行時候用malloc或new申請任意大小的記憶體,程式員自己負責在適當的時候用free或delete釋放記憶體(Java則依賴記憶體回收行程)。動態記憶體的生存期可以由我們決定,如果我們不釋放記憶體,程式將在最後才釋放掉動態記憶體。 但是,良好的編程習慣是:如果某動態記憶體不再使用,需要將其釋放掉。
堆和棧的區別:
在函數中(說明是局部變數)定義的一些基本類型的變數和對象的引用變數都是在函數的棧記憶體中分配。當在一段代碼塊中定義一個變數時,java就在棧中為這個變數分配記憶體空間,當超過變數的範圍後,java會自動釋放掉為該變數分配的記憶體空間,該記憶體空間可以立刻被另作他用。
堆記憶體用於存放所有由new建立的對象(內容包括該對象其中的所有成員變數)和數組。在堆中分配的記憶體,由java虛擬機器自動記憶體回收行程來管理。在堆中產生了一個數組或者對象後,還可以在棧中定義一個特殊的變數,這個變數的取值等於數組或者對象在堆記憶體中的首地址,在棧中的這個特殊的變數就變成了數組或者對象的引用變數,以後就可以在程式中使用棧記憶體中的引用變數來訪問堆中的數組或者對象,引用變數相當於為數組或者對象起的一個別名,或者代號
堆是不連續的記憶體地區(因為系統是用鏈表來儲存空閑記憶體位址,自然不是連續的),堆大小受限於電腦系統中有效虛擬記憶體(32bit系統理論上是4G),所以堆的空間比較靈活,比較大。棧是一塊連續的記憶體地區,大小是作業系統預定好的,windows下棧大小是2M(也有是1M,在編譯時間確定,VC中可設定)。
對於堆,頻繁的new/delete會造成大量記憶體片段,使程式效率降低。對於棧,它是先進後出的隊列,進出一一對應,不產生片段,運行效率穩定高。
所以我們可以得出結論:
1).局部變數的基礎資料型別 (Elementary Data Type)和引用儲存於棧中,引用的對象實體儲存於堆中。因為它們屬於方法中的變數,生命週期隨方法而結束。
2).成員變數全部儲存與堆中(包括基礎資料型別 (Elementary Data Type),引用和引用的對象實體),因為它們屬於類,類對象終究是要被new出來使用的。
3).我們所說的記憶體泄露,只針對堆記憶體,他們存放的就是引用指向的對象實體。
2.記憶體泄露產生的原因
在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式員不需要通過調用函數來釋放記憶體,但它只能回收無用並且不再被其它對象引用的那些對象所佔用的空間。
Java的記憶體記憶體回收機制是從程式的主要運行對象(如靜態對象/寄存器/棧上指向的堆記憶體對象等)開始檢查引用鏈,當遍曆一遍後得到上述這些無法回收的對象和他們所引用的對象鏈,組成無法回收的對象集合,而其他孤立對象(集)就作為記憶體回收。GC為了能夠正確釋放對象,必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。監視對象狀態是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。
在Java中,這些無用的對象都由GC負責回收,因此程式員不需要考慮這部分的記憶體泄露。雖然,我們有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規範定義,該函數不保證JVM的垃圾收集器一定會執行。因為不同的JVM實現者可能使用不同的演算法管理GC。通常GC的線程的優先順序別較低。JVM調用GC的策略也有很多種,有的是記憶體使用量到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。
但是我們仍然可以去監聽系統的GC過程,以此來分析我們應用程式當前的記憶體狀態。那麼怎樣才能去監聽系統的GC過程呢?其實非常簡單,系統每進行一次GC操作時,都會在LogCat中列印一條日誌,我們只要去分析這條日誌就可以了,日誌的基本格式如下所示:
1.D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
首先第一部分GC_Reason,這個是觸發這次GC操作的原因,一般情況下一共有以下幾種觸發GC操作的原因:
GC_CONCURRENT: 當我們應用程式的堆記憶體快要滿的時候,系統會自動觸發GC操作來釋放記憶體。
GC_FOR_MALLOC: 當我們的應用程式需要分配更多記憶體,可是現有記憶體已經不足的時候,系統會進行GC操作來釋放記憶體。
GC_HPROF_DUMP_HEAP: 當產生HPROF檔案的時候,系統會進行GC操作,關於HPROF檔案我們下面會講到。
GC_EXPLICIT: 這種情況就是我們剛才提到過的,主動通知系統去進行GC操作,比如調用System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的。
接下來第二部分Amount_freed,表示系統通過這次GC操作釋放了多少記憶體。
然後Heap_stats中會顯示當前記憶體的空閑比例以及使用方式(使用中的物件所佔記憶體 / 當前程式總記憶體)。
最後Pause_time表示這次GC操作導致應用程式暫停時間。關於這個暫停時間,Android在2.3的版本當中進行過一次最佳化,在2.3之前GC操作是不能並發進行的,也就是系統進行中GC,那麼應用程式就只能阻塞住等待GC結束。雖說這個阻塞的過程並不會很長,也就是幾百毫秒,但是使用者在使用我們的程式時還是有可能會感覺到略微的卡頓。而自2.3之後,GC操作改成了並發的方式進行,就是說GC的過程中不會影響到應用程式的正常運行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間,不過最佳化到這種程度,使用者已經是完全無法察覺到了。
我們來看看Java中需要被回收的垃圾:
{Person p1 = new Person();……}
引用控制代碼p1的範圍是從定義到“}”處,執行完這對大括弧中的所有代碼後,產生的Person對象就會變成垃圾,因為引用這個對象的控制代碼p1已超過其範圍,p1失效,在棧中被銷毀,因此堆上的Person對象不再被任何控制代碼引用了。 因此person變為垃圾,會被回收。
這裡我們需要講述一個關鍵詞:引用,通過A能調用並訪問到B,那就說明A持有B的引用,或A就是B的引用,B的引用計數+1.
(1)比如 Person p1 = new Person();通過P1能操作Person對象,因此P1是Person的引用;
(2)比如類O中有一個成員變數是I類對象,因此我們可以使用o.i的方式來訪問I類對象的成員,因此o持有一個i對象的引用。
GC過程與對象的參考型別是嚴重相關的,我們來看看Java對引用的分類Strong reference, SoftReference, WeakReference, PhatomReference
在Android應用的開發中,為了防止記憶體溢出,在處理一些佔用記憶體大而且聲明周期較長的對象時候,可以盡量應用軟引用和弱引用技術。
軟/弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被記憶體回收行程回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩衝器清除已失效的軟/弱引用。
假設我們的應用會用到大量的預設圖片,比如應用中有預設的頭像,預設遊戲表徵圖等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取檔案需要硬體操作,速度較慢,會導致效能較低。所以我們考慮將圖片緩衝起來,需要的時候直接從記憶體中讀取。但是,由於圖片佔用記憶體空間比較大,緩衝很多圖片需要很多的記憶體,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術來避免這個問題發生。以下就是高速緩衝器的雛形:
首先定義一個HashMap,儲存軟引用對象。
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
再來定義一個方法,儲存Bitmap的軟引用到HashMap
public class CacheSoftRef { //首先定義一個HashMap,儲存引用對象 private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); //再來定義一個方法,儲存Bitmap的軟引用到HashMap public void addBitmapToCache(String path) { //強引用的Bitmap對象 Bitmap bitmap = BitmapFactory.decodeFile(path); //軟引用的Bitmap對象 SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap); //添加該對象到Map中使其緩衝 imageCache.put(path, softBitmap); } //擷取的時候,可以通過SoftReference的get()方法得到Bitmap對象 public Bitmap getBitmapByPath(String path) { //從緩衝中取軟引用的Bitmap對象 SoftReference<Bitmap> softBitmap = imageCache.get(path); //判斷是否存在軟引用 if (softBitmap == null) { return null; } //通過軟引用取出Bitmap對象,如果由於記憶體不足Bitmap被回收,將取得空,如果未被回收, //則可重複使用,提高速度。 Bitmap bitmap = softBitmap.get(); return bitmap; }}
使用軟引用以後,在OutOfMemory異常發生之前,這些緩衝的圖片資源的記憶體空間可以被釋放掉的,從而避免記憶體達到上限,避免Crash發生。
如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的效能更在意,想儘快回收一些佔用記憶體比較大的對象,則可以使用弱引用。
另外可以根據對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。
所以我們得出記憶體流失的原因:堆記憶體中的長生命週期的對象持有短生命週期對象的強/軟引用,儘管短生命週期對象已經不再需要,但是因為長生命週期對象持有它的引用而導致不能被回收,這就是Java中記憶體泄露的根本原因。
3.記憶體流失的檢測
Allocation Tracker(Device Monitor)
Allocation Tracker操作
1.首先進入你要追蹤的介面
2.點擊Start Tracking按鈕,開始跟蹤記憶體配置軌跡
3.操作你的介面,盡量時間短點
4.點擊Get Allocations按鈕,抓去記憶體配置軌跡資訊,顯示在右邊的面板中,預設以記憶體大小排序,你可以以分配順序排序或者仍以列排序。
5.logcat中會顯示出這次的軌跡共抓到記憶體配置軌跡記錄數,可以簡單的理解分配了多少次記憶體,這個數值和Alloc order的最大值是相等的
6.如果你不想看那麼多亂七八糟的,你可以使用Filter來過濾,輸入包名就可以了。
跟蹤記憶體軌跡
如果這個時候我們想單獨擷取某次操作的記憶體軌跡,首先一定要記得Stop Tracking再Start Tracking一下,讓追蹤點初始化一下,然後就進行我們需要觀察記憶體變化的操作,然後點擊Get Allocations,這個時候我們從首頁進入一個詳情頁,看一下我們的記憶體配置軌跡:
中,我們可以看出來,在第635次記憶體配置中,分配的是IntroduceFragment對象,佔用記憶體224位元組,處理線程Id為3245,在java.lang.Class的newInstance方法中分配的。從trace資訊可以看出來該方法一步一步被調用的資訊。
DDMS的Heap Viewer在Devices 中,點擊要監控的程式。
點擊Devices視圖介面中最上方一排表徵圖中的“Update Heap”
點擊Heap視圖
點擊Heap視圖中的“Cause GC”按鈕
到此為止需檢測的進程就可以被監視。
按的標記順序按下,我們就能看到記憶體的具體資料,右邊面板中數值會在每次GC時發生改變,包括App自動觸發或者你來手動觸發。
MAT工具那麼通過上面DDMS工具,現在我們已經可以比較輕鬆地發現應用程式中是否存在記憶體泄露的現象了。
我們應該怎麼定位到具體是哪裡出的記憶體泄露呢?這就需要藉助一個記憶體分析工具了,叫做Eclipse Memory Analyzer(MAT)。
是:http://eclipse.org/mat/downloads.php。
LeakCanaryLeakCanary是Square開源了一個記憶體泄露自動探測神器 。這是項目的github倉庫地址:https://github.com/square/leakcanary 。使用非常簡單,在build.gradle中引入包依賴:
debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.5‘releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5‘testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5‘在Application中的onCreate方法中增加初始化代碼:if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for // heap analysis. // You should not init your app in this process. return;}LeakCanary.install(this);
整合後什麼都不用做,按照正常測試,當有記憶體流失發生後,應用會通過系統通知欄發出通知,點擊通知就可以進入查看記憶體流失的具體資訊。在這裡舉個實踐中的例子。把LeakCanary整合到項目中後,等App啟動後一會,系統通知到了,點擊通知,跳轉到泄漏的詳情頁面進行查看:
很明顯,WebSiteQueryActivity泄露了。首先,static 的MaskHeadView.fLayout變數引用了FrameLayout.mContext對象,這個對象的引用就是指向了WebSiteQueryActivity的執行個體,導致了它的泄漏,在第二節中我們說過static對象是內部的static對象是比較容易造成記憶體流失的,檢查代碼發現,MaskHeadView直接在WebSiteQueryActivity的xml檔案中使用了,因此持有WebSiteQueryActivity的執行個體,因為fLayout對象是靜態,因此它的生命週期與Application同樣長,因此WebSiteQueryActivity退出後,它的執行個體引用依然被fLayout持有,導致它無法被回收從而記憶體泄露了。仔細檢查代碼,發現fLayout並沒有被外部使用到,應該是之前的開發人員手抖加了個static欄位上去或者是現在不用了,但是沒有去掉,在這裡我直接去掉了這個修飾符,在此build代碼,這個記憶體流失的現象消失了。
//去掉static修飾符,避免static對象引起的記憶體流失private static FrameLayout fLayout;public MaskHeadView(Context context, AttributeSet attrs) { super(context, attrs); this.context=context; initView(context);}private void initView(Context context2) { view = LayoutInflater.from(context).inflate(R.layout. mask_head_view, this); fLayout=(FrameLayout) view.findViewById(R.id. mask_container);}
無論是MAT工具的記憶體分析,還是AndroidStudio中內建的分析工具亦或是LeakCanary,原理都是一樣的,都是dump java heap出來進行分析,找到泄漏的問題,只是LeakCanary幫我們把分析的工作做了。但值得一提的是,LeakCanary並不是萬能的,有些記憶體流失,它也無法檢測出來。
Android效能最佳化(四):記憶體最佳化