Android內存分析

Android內存分析

  在進行Android開發時,OOM是一類非常難處理的問題,要處理OOM問題,除了在編程時多注意對內存的使用,還要會對內存的使用情況進行分析。
  現在Android手機的內存已經非常大,但這不能成爲應用程序開發者,不注意內存使用的理由,一方面,內存使用過高,會對整個app性能產生影響,另一方面對每個應用程序來說,系統是不會把所有內存分配給一個應用程序的,每個應用程序都有內存的使用上限,被稱爲堆大小。不同的手機,堆大小也不同。可以通過以下方法,得到當前手機的堆大小。

ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);  
int heapSize = manager.getMemoryClass();  

  該方法返回的結果以MB爲單位,如果應用程序使用內存超過該值,將出現OOM。

  • 分析Log信息
  • 使用工具進行具體分析

分析Log信息

  進行Android內存分析最簡單的開始就是分析運行時輸出的Log。當發生GC時,會有一條相關的logcat信息輸出。

Dalvik Log信息

  在Dalvik中,每一次GC打印的logcat信息格式如下:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

真實輸出的示例如下:

D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms

參數字段講解:

GC Reason

觸發本次GC的原因,包含的值如下:

  • GC_CONCURRENT:當應用程序的堆內存快滿時,系統會觸發該種GC以釋放內存。
  • GC_FOR_MALLOC:當應用程序需要更多內存,但堆內存不足時,系統觸發該種GC以釋放內存。
  • GC_HPROF_DUMP_HEAP:當爲了分析堆內存去請求創建一個HPROF文件時,系統觸發該種GC。
  • GC_EXPLICIT:主動通知系統執行GC操作時,觸發該種GC,比如System.gc()方法。
  • GC_EXTERNAL_ALLOC:只在API Level 10或更低時,纔會觸發該種GC。爲外部的內存分配而發生的GC(例如,存儲在本地內存或NIO二進制緩存中的像素數據)。
Amount freed

  本次GC回收的內存總量。

Heap stats

  本次釋放掉內存的百分比,和(存活對象佔用內存)/(總堆大小)

External memory stats

  只在API Level 10或更低時,纔有該字段。表示外部分配的內存狀態,(分配的數量)/(發生GC的限制)。

Pause time

  更大的堆會有更長的停滯時間。該字段同時出現兩個暫停時間:一個是收集的開始,另一個接近結束。
  當打印大量這種Log信息,對其進行分析,發現存活對象佔用內存的值是持續上升的,可能就發生了內存泄漏。

ART Log信息

  不像Dalvik,ART在沒有明確請求打印log時,不會打印信息。GC信息僅在被認爲GC緩慢時才被打印。更準確的說,如果GC暫停超過5ms或GC持續超過100ms。如果應用不在可察覺的進程停滯狀態,則沒有GC被認爲是緩慢的。主動的請求GC每次都會被打印。
  在ART中,打印出的GC log的格式如下:

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>

真實輸出的示例如下:

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

參數字段講解:

GC Reason

觸發本次GC的原因,包含的值如下:

  • Concurrent:併發的GC不會停滯所有線程,這種GC運行在後臺,不會阻止內存的分配。
  • Alloc:這種GC,在應用需要更多內存,但堆內存不足時被觸發。在這種情況,垃圾收集發生在分配線程中。
  • Explicit:當應用主動請求GC時,該種GC被觸發,比如System.gc()方法。同Dalvik一樣,在ART中也推薦信任系統主動的垃圾收集而避免進行主動的GC請求。
  • NativeAlloc:當本地內存分配對本地內存產生壓力時,該種GC被觸發,例如位圖(Bitmaps)或渲染腳本(RenderScript)分配對象。
  • CollectorTransition:該種GC由“堆轉換”(在運行時切換GC)引起。收集器轉換包括將“空閒列表”空間的所有對象複製“碰撞指針”空間。當前的收集器轉換僅發生在低內存設備上的應用將進程狀態從可察覺的停滯狀態轉變爲不可察覺的停滯狀態。
  • HomogeneousSpaceCompact(齊次空間緊緻):內存的齊次空間壓縮是一個“空閒列表”空間到一個壓縮的“空閒列表”空間的過程,它通常發生在應用被轉變到可察覺進程停滯狀態時。這種GC的主要目的是減少內存的使用和整理內存碎片。
  • DisableMovingGc:這不是一種真正的GC原因。而是由於GetPrimitiveArrayCritical的使用,導致垃圾收集阻塞的一個提示。
  • HeapTrim(堆裁剪):這也不是一種真正的GC原因。而是垃圾收集會被阻塞到堆整理完成的一個提示。
GC Name

ART有多種不同的垃圾收集器可被運行。

  • Concurrent mark sweep (CMS)(併發標記清除):一個完整的堆收集器,釋放除了圖片空間之外的所有空間
  • Concurrent partial mark sweep(併發部分標記清除):一個部分完整的堆收集器,收集除了圖片和zygote之外的空間
  • Concurrent sticky mark sweep(併發粘性標記清除):一個代際收集器,只能釋放從最近一次GC以來分配的對象。這個垃圾收集器比CMS和CPMS運行的更普遍,因爲它的速度更快,阻塞更低
  • Marksweep + semispace:一個非併發的、備份GC,用於堆轉換和同類空間壓縮(相對堆碎片整理而言)
Objects freed

  本次GC從不是“大對象”的空間釋放的對象數。

Size freed

  本次GC從不是“大對象”的空間釋放的字節數。

Large objects freed

  本次GC在“大對象”空間釋放的的對象數。

Large object size freed

  本次GC在“大對象”空間釋放的的字節數。

Heap stats

  本次釋放掉內存的百分比,和(存活對象佔用內存)/(總堆大小)

Pause times

  通常,GC運行時的停滯時間和引用變換的對象的數量是成正比的。目前,ART CMS收集僅在接近GC結束的時候有一次停滯。移動收集則有一個長的停滯時間,它佔了GC期間的絕大多數時間。

  如果你看見了大量的GC Log,則主要查看Heap stats的增長部分(例子中的25MB/38MB)。如果這部分持續增長,則你的應用程序存在內存泄漏的可能。與此同時,如果你看到的GC Reason是“Alloc”,則你的應用內存的使用已經接近堆的容量,很快就可能出現OOM異常。

使用工具進行具體分析

  在使用工具進行具體內存使用情況分析時,一般先使用Android Studio的Memory Monitor工具觀察應用內存的使用狀況和GC表現。然後在感覺內存使用有問題的地方,獲取hprof文件,並使用Eclipse Memory Analyzer(MAT)工具對該文件進行分析,從而完成對內存使用狀況的分析,發現其中的內存使用問題,優化程序。
  接下來通過對下面示例程序的內存使用情況進行分析,從而詳細的介紹分析過程。

public class StaticReferenceActivity extends AppCompatActivity {

    private static final String NAME = StaticReferenceActivity.class.getSimpleName();
    private static final String TAG = "sxd";

    public static StaticReferenceActivity sStaticReferenceActivity;
    private static Bitmap sBitmap;

    private ImageView mImageView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_static_reference);
        sStaticReferenceActivity = this;
        initView();
    }

    private void initView() {
        mImageView = (ImageView) this.findViewById(R.id.image_view);
        sBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.image);
        mImageView.setImageBitmap(sBitmap);![Alt text](./觀測.PNG)

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, NAME + "--onDestroy++");
    }

}

  上面示例程序,因爲使用了靜態變量引用資源,所以在應用程序進入該Activity後,將發生內存泄漏。

使用Android Studio的Memory Monitor工具觀測內存使用

Memory Monitor觀測內存說明

這裏寫圖片描述

  上圖中,動態圖表的下面深色部分表示當前應用已分配的內存,上面的淺色部分表示當前時刻釋放的內存。
這裏寫圖片描述

  如上圖所示動態圖表中的每一抖動都代表一次GC事件。

對示例程序內存使用情況進行監測

  首先,進入示例應用程序,點擊Memory Monitor左邊的這裏寫圖片描述按鈕,進行一次GC,然後進入StaticReferenceActivity界面,再退出該界面,最後再點擊這裏寫圖片描述按鈕執行一遍GC。執行完這個過程的觀測結果如下:
這裏寫圖片描述

  從圖中可以看出,示例程序在進入StaticReferenceActivity退出後,執行GC後,並沒有完全回收掉進入時分配的內存。所以我們懷疑在StaticReferenceActivity界面中對內存的使用出了問題。但具體是什麼問題?有時簡單的情況下,可能查看代碼就很容易發現,我們的示例程序的內存泄漏原因就很容易發現,但這只是爲了演示工具的使用,但很多情況下,我們並不能很快通過查閱代碼找出內存泄漏問題,這就需要使用工具進行進一步分析定位。

獲取hprof文件

  在觀測到內存使用問題後,可以點擊左邊的這裏寫圖片描述按鈕,來生成一個hprof文件,該文件記錄着我們應用程序內部的所有數據。但剛生成的這種hprof文件還不能使用MAT工具直接打開,進行分析。我們需要使用下面的命令將hprof該文件從Dalvik格式轉換成J2SE格式。
這裏寫圖片描述

  轉換輸出的hprof文件就可以被MAT工具打開,進行分析了。

使用Eclipse Memory Analyzer(MAT)工具,對hprof文件進行分析

打開hporf文件

這裏寫圖片描述

  上圖最中央的那個餅狀圖展示了最大的幾個對象所佔內存的比例,這張圖中提供的內容並不多,所以不對其進行說明。在這個餅狀圖的下方就有幾個非常有用的工具了。其中最主要就是上圖中標紅的兩個工具。

  • Histogram:可以列出內存中每個對象的名字、數量以及大小
  • Dominator Tree:會將所有內存中的對象按大小進行排序,並且我們可以分析對象之間的引用結構
Dominator Tree

這裏寫圖片描述

  下面開始對上圖展示的信息進行解讀。
  Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所佔的總內存。Shallow Heap則表示當前對象自己所佔內存的大小,不包含引用關係。
  我們還發現,在每一行最左邊的文件型圖標的左下角,有的會帶有一個紅色的點。帶有紅點的對象就表示是可以被GC Roots(相關內容查看“Java內存分配與垃圾收集”一文)訪問到的,根據上面的講解,可以被GC Root訪問到的對象都是無法被回收的。但不代表沒有紅點的對象類型的對象就一定不會被GC Roots訪問到。我們也可以發現,每一行都可以被展開,展開後可以看到當前存在於程序中的該種類型的對象數。有的對象類型的後面還有System Class,其說明該種類型是一個系統類型,而不是由用戶創建的。
  在用Dominator Tree分析內存問題時,我們首先對Retained Heap中最大的進行分析,佔用內存最大的部分,也是最容易出現內存泄漏的部分。要分析每一類型對象的內存使用情況,則在其上點擊右鍵 -> Path to GC Roots -> exclude weak references(弱引用可在GC時釋放)。下面是在Bitmap類型上執行上述操作的結果:
這裏寫圖片描述

  從圖中可以很容易的發現,在StaticReferenceActivity中有一個名爲sBitmap的Bitmap對象可以被GC Roots訪問到,導致這部分內存不能被釋放,我們查看示例程序發現,該對象在程序中被設置爲靜態,我們知道靜態變量會被放入靜態存儲區中,它的生命週期同應用程序一樣長,所以該部分發生內存泄漏。

Histogram

這裏寫圖片描述

  使用Histogram進行內存分析,也是先從佔用內存大的對象開始分析。但更經常的是,使用Histogram對你認爲可能會造成內存泄漏的對象進行精確分析,例如在我們的示例中,我們進入StaticReferenceActivity界面並退出之後,仍然有一部分內存不能被GC掉,所以我們懷疑StaticReferenceActivity這個對象出現了內存泄漏。我們在Histogram界面最上面的輸入框,輸入StaticReferenceActivity後,結果如下:
這裏寫圖片描述

  果然程序中還存在一個StaticReferenceActivity沒有被回收掉。接下來我們分析具體原因,我們在StaticReferenceActivity上右鍵-> List objects -> with incoming references。查看具體StaticReferenceActivity實例。然後在實例上右鍵 -> Path to GC Roots -> exclude weak references。結果如下圖:
這裏寫圖片描述

  從上圖中可以很容易的發現,在StaticReferenceActivity中有一個名爲sStaticReferenceActivity的StaticReferenceActivity對象可以被GC Roots訪問到,導致該StaticReferenceActivity實例不能被消耗回收。我們查看示例程序發現,該對象在程序中被設置爲靜態,我們知道靜態變量會被放入靜態存儲區中,它的生命週期同應用程序一樣長,所以該部分發生內存泄漏。

  上面簡單的說明了查看GC Log和使用分析工具,對android應用內存使用情況進行分析的方法。

源碼地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章