1. 程式人生 > >Android Studio Profiler Memory (記憶體分析工具)的簡單使用及問題

Android Studio Profiler Memory (記憶體分析工具)的簡單使用及問題

Memory Profiler 是 Android Studio自帶的記憶體分析工具,可以幫助開發者很好的檢測記憶體的使用,在出現問題時,也能比較方便的分析定位問題,不過在使用的時候,好像並非像自己一開始設想的樣子。

如何檢視整體的記憶體使用概況

如果想要看一個APP整體記憶體的使用,看APP heap就可以了,不過需要注意Shallow Size跟Retained Size是意義,另外native消耗的記憶體是不會被算到Java堆中去的。

image.png

  • Allocations:堆中的例項數。
  • Shallow Size:此堆中所有例項的總大小(以位元組為單位)。其實算是比較真實的java堆記憶體
  • Retained Size:為此類的所有例項而保留的記憶體總大小(以位元組為單位)。這個解釋並不準確,因為Retained Size會有大量的重複統計
  • native size:8.0之後的手機會顯示,主要反應Bitmap所使用的畫素記憶體(8.0之後,轉移到了native)

舉個例子,建立一個List的場景,有一個ListItem40MClass類,自身佔用40M記憶體,每個物件有個指向下一個ListItem40MClass物件的引用,從而構成List,

class ListItem40MClass {

    byte[] content = new byte[1000 * 1000 * 40];
    ListItem40MClass() {
        for (int i = 0; i < content.length; i++) {
            content[i] = 1;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }

    ListItem40MClass next;
}


@OnClick(R.id.first)
void first() {
    if (head == null) {
        head = new ListItem40MClass();
    } else {
        ListItem40MClass tmp = head;
        while (tmp.next != null) {
            tmp = tmp.next;
        }
        tmp.next = new ListItem40MClass();
    }
}
複製程式碼

我們建立三個這樣的物件,並形成List,示意如下

A1->next=A2
A2->next=A3 
A3->next= null
複製程式碼

這個時候用Android Profiler檢視記憶體,會看到如下效果:Retained Size統計要比實際3個ListItem40MClass類物件的大小大的多,如下圖:

281540022720_.pic_hd.jpg

可以看到就總量而言Shallow Size基本能真是反應Java堆記憶體,而Retained Size卻明顯要高出不少, 因為Retained Size統計總記憶體的時候,基本不能避免重複統計的問題,比如:A物件有B物件的引用在計算總的物件大小的時候,一般會多出一個B,就像上圖,有個3個約40M的int[]物件,佔記憶體約120M,而每個ListItem40MClass物件至少會再統計一次40M,這裡說的是至少,因為物件間可能還有其他關係。我們看下單個類的記憶體佔用-Instance View

  • Depth:從任意 GC 根到所選例項的最短 hop 數。
  • Shallow Size:此例項的大小。
  • Retained Size:此例項支配的記憶體大小(根據 dominator 樹)。

可以看到Head本身的Retained Size是120M ,Head->next 是80M,最後一個ListItem40MClass物件是40M,因為每個物件的Retained Size除了包括自己的大小,還包括引用物件的大小,整個類的Retained Size大小累加起來就大了很多,所以如果想要看整體記憶體佔用,看Shallow Size還是相對準確的,Retained Size可以用來大概反應哪種類佔的記憶體比較多,僅僅是個示意,不過還是Retained Size比較常用,因為Shallow Size的大戶一般都是String,陣列,基本型別意義不大,如下。

291540025853_.pic.jpg

FinalizerReference大小跟記憶體使用及記憶體洩漏的關係

之前說Retained Size是此例項支配的記憶體大小,其實在Retained Size的統計上有很多限制,比如Depth:從任意 GC 根到所選例項的最短hop數,一個物件的Retained Size只會統計Depth比自己大的引用,而不會統計小的,這個可能是為了避免重複統計而引入的,但是其實Retained Size在整體上是免不了重複統計的問題,所以才會右下圖的情況:

image.png

FinalizerReference中refrent的物件的retain size是40M,但是沒有被計算到FinalizerReference的retain size中去,而且就圖表而言FinalizerReference的意義其實不大,FinalizerReference物件本身佔用的記憶體不大,其次FinalizerReference的retain size統計的可以說是FinalizerReference的重複累加的和,並不代表其引用物件的大小,僅僅是ReferenceQueue queue中ReferenceQueue的累加,

public final class FinalizerReference<T> extends Reference<T> {
    // This queue contains those objects eligible for finalization.
    public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();

    // Guards the list (not the queue).
    private static final Object LIST_LOCK = new Object();

    // This list contains a FinalizerReference for every finalizable object in the heap.
    // Objects in this list may or may not be eligible for finalization yet.
    private static FinalizerReference<?> head = null;

    // The links used to construct the list.
    private FinalizerReference<?> prev;
    private FinalizerReference<?> next;

    // When the GC wants something finalized, it moves it from the 'referent' field to
    // the 'zombie' field instead.
    private T zombie;

    public FinalizerReference(T r, ReferenceQueue<? super T> q) {
        super(r, q);
    }

    @Override public T get() {
        return zombie;
    }

    @Override public void clear() {
        zombie = null;
    }

    public static void add(Object referent) {
        FinalizerReference<?> reference = new FinalizerReference<Object>(referent, queue);
        synchronized (LIST_LOCK) {
            reference.prev = null;
            reference.next = head;
            if (head != null) {
                head.prev = reference;
            }
            head = reference;
        }
    }

    public static void remove(FinalizerReference<?> reference) {
        synchronized (LIST_LOCK) {
            FinalizerReference<?> next = reference.next;
            FinalizerReference<?> prev = reference.prev;
            reference.next = null;
            reference.prev = null;
            if (prev != null) {
                prev.next = next;
            } else {
                head = next;
            }
            if (next != null) {
                next.prev = prev;
            }
        }
    }
...
}
複製程式碼

每個FinalizerReference retained size 都是其next+ FinalizerReference的shallowsize,反應的並不是其refrent物件記憶體的大小,如下:

image.png

因此FinalizerReference越大隻能說明需要執行finalize的物件越多,並且物件是通過強引用被持有,等待Deamon執行緒回收。可以通過該下程式碼試驗下:

 class ListItem40MClass {
        byte[] content = new byte[5];

        ListItem40MClass() {
            for (int i = 0; i < content.length; i += 1000) {
                content[i] = 1;
            }
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            LogUtils.v("finalize ListItem40MClass");
        }

        ListItem40MClass next;
    }


    @OnClick(R.id.first)
    void first() {
        if (head == null) {
            head = new ListItem40MClass();
        } else {
            for (int i = 0; i < 1000; i++) {
                ListItem40MClass tmp = head;
                while (tmp.next != null) {
                    tmp = tmp.next;
                }
                tmp.next = new ListItem40MClass();
            }
        }
    }
複製程式碼

多次點選後,可以看到finalize的物件線性上升,而FinalizerReference的retain size卻會指數上升。

image.png

同之前40M的對比下,明顯上一個記憶體佔用更多,但是其實FinalizerReference的retain size卻更小。再來理解FinalizerReference跟記憶體洩漏的關係就比價好理解了,回收執行緒沒執行,實現了finalize方法的物件一直沒有被釋放,或者很遲才被釋放,這個時候其實就算是洩漏了。

如何看Profiler的Memory圖

  • 第一:看整體Java記憶體使用看shallowsize就可以了
  • 第二:想要看哪些物件佔用記憶體較多,可以看Retained Size,不過看Retained Size的時候,要注意過濾一些無用的比如 FinalizerReference,基本型別如:陣列物件

比如下圖:Android 6.0 nexus5

image.png

從整體概況上看,Java堆記憶體的消耗是91兆左右,而整體的shallow size大概80M,其餘應該是一些堆疊基礎型別的消耗,而在Java堆疊中,佔比最大的是byte[],其次是Bitmap,bitmap中的byte[]也被算進了前面的byte[] retain size中,而FinilizerReference的retain size已經大的不像話,沒什麼參考價值,可以看到Bitmap本身其實佔用記憶體很少,主要是裡面的byte[],當然這個是Android8.0之前的bitmap,8.0之後,bitmap的記憶體分配被轉移到了native。

再來對比下Android8.0的nexus6p:可以看到佔大頭的Bitmap的記憶體轉移到native中去了,降低了OOM風險。

image.png

並且在Android 8.0或更高版本中,可以更清楚的檢視物件及記憶體的動態分配,而且不用dump記憶體,直接選中某一段,就可以看這個時間段的記憶體分配:如下

image.png

如上圖,在時間點1 ,我們建立了一個物件new ListItem40MClass(),ListItem40MClass有一個比較佔記憶體的byte陣列,上面折線升高處有新物件建立,然後會發現記憶體大戶是byte陣列,而最新的byte陣列是在ListItem40MClass物件建立的時候分配的,這樣就能比較方便的看到,到底是哪些物件導致的記憶體上升。

總結

  • 總體Java記憶體使用看shallow size
  • retained size只是個參考,不準確,存在各種重複統計問題
  • FinalizerReference retained size 大小極其不準確,而且其強引用的物件並沒有被算進去,不過finilize確實可能導致記憶體洩漏
  • native size再8.0之後,對Bitmap的觀測有幫助。