Android Studio Profiler Memory (記憶體分析工具)的簡單使用及問題
Memory Profiler 是 Android Studio自帶的記憶體分析工具,可以幫助開發者很好的檢測記憶體的使用,在出現問題時,也能比較方便的分析定位問題,不過在使用的時候,好像並非像自己一開始設想的樣子。
如何檢視整體的記憶體使用概況
如果想要看一個APP整體記憶體的使用,看APP heap就可以了,不過需要注意Shallow Size跟Retained Size是意義,另外native消耗的記憶體是不會被算到Java堆中去的。
- 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類物件的大小大的多,如下圖:
可以看到就總量而言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,陣列,基本型別意義不大,如下。
FinalizerReference大小跟記憶體使用及記憶體洩漏的關係
之前說Retained Size是此例項支配的記憶體大小,其實在Retained Size的統計上有很多限制,比如Depth:從任意 GC 根到所選例項的最短hop數,一個物件的Retained Size只會統計Depth比自己大的引用,而不會統計小的,這個可能是為了避免重複統計而引入的,但是其實Retained Size在整體上是免不了重複統計的問題,所以才會右下圖的情況:
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物件記憶體的大小,如下:
因此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卻會指數上升。
同之前40M的對比下,明顯上一個記憶體佔用更多,但是其實FinalizerReference的retain size卻更小。再來理解FinalizerReference跟記憶體洩漏的關係就比價好理解了,回收執行緒沒執行,實現了finalize方法的物件一直沒有被釋放,或者很遲才被釋放,這個時候其實就算是洩漏了。
如何看Profiler的Memory圖
- 第一:看整體Java記憶體使用看shallowsize就可以了
- 第二:想要看哪些物件佔用記憶體較多,可以看Retained Size,不過看Retained Size的時候,要注意過濾一些無用的比如 FinalizerReference,基本型別如:陣列物件
比如下圖:Android 6.0 nexus5
從整體概況上看,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風險。
並且在Android 8.0或更高版本中,可以更清楚的檢視物件及記憶體的動態分配,而且不用dump記憶體,直接選中某一段,就可以看這個時間段的記憶體分配:如下
如上圖,在時間點1 ,我們建立了一個物件new ListItem40MClass(),ListItem40MClass有一個比較佔記憶體的byte陣列,上面折線升高處有新物件建立,然後會發現記憶體大戶是byte陣列,而最新的byte陣列是在ListItem40MClass物件建立的時候分配的,這樣就能比較方便的看到,到底是哪些物件導致的記憶體上升。
總結
- 總體Java記憶體使用看shallow size
- retained size只是個參考,不準確,存在各種重複統計問題
- FinalizerReference retained size 大小極其不準確,而且其強引用的物件並沒有被算進去,不過finilize確實可能導致記憶體洩漏
- native size再8.0之後,對Bitmap的觀測有幫助。