Android性能優化(三)內存管理

內存管理

內存模型

    Android原生開以java主。

在java中,Java內存模型,往往是指Java程序在運行時內存的模型,而Java代碼是運行在Java虛擬機之上的,所以Java內存模型,也就是指Java虛擬機的運行時內存模型。

 

       java中內存全權交給虛擬機去管理,那虛擬機的運行時內存是如何構成的?

       

很多時候,我們提到內存,會說到堆和棧,這是對內存粗略的一種劃分,這種劃分的對應內存模型的Java堆,是指虛擬機棧,但是實際上Java內存模型比這複雜多了。

       

       在曾經的日公司(sun 已被甲骨文2009收購制定的java虛擬機規範中,運行時內存模型,分爲線程私有和共享數據區兩大類,其中線程私有的數據區包含程序計數器、虛擬機棧、本地方法區,所有線程共享的數據區包含Java堆、方法區,在方法區內有一個常量池。

 

       

 

2.1 程序數器PC

程序計數器PC是一塊較小的內存空間,可以看作所執行字節碼的行號指示器。字節碼解釋器就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,比如循環、跳轉、異常處理等等這些基礎功能都需要依賴這個計數器來完成。

在開發多線程應用時,由於Java中的多線程是搶佔式的調用,也就是任何一個確定的時刻,cpu都只會執行一條線程,執行哪條線程也是不確定的。所以爲了線程切換後能夠回到正確的執行位置,每個線程都需要一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,所以這塊區域是線程私有的內存。

當線程正在執行一個Java方法時,PC計數器記錄的是正在執行的虛擬機字節碼的地址;當線程正在執行的一個Native方法時,PC計數器則爲空(Undefined)。這一塊的內存區域是唯一一個在java虛擬機規範中沒有規定任何OutOfMemoryError的區域。

2.2 

和程序計數器一樣,虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是java方法執行的內存模型。

每個方法(不包含native方法)執行的同時都會創建一個棧幀 用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

       我們平時所說的棧內存就是指的這一塊區域。

Java虛擬機規範規定該區域有兩種異常:

  • StackOverFlowError:當線程請求棧深度超出虛擬機棧所允許的深度時拋出 (遞歸函數)
  • OutOfMemoryError:當Java虛擬機動態擴展到無法申請足夠內存時拋出 (OOM)

2.3 本地方法

本地方法棧和虛擬機棧差不多,前者是爲虛擬機使用到的Native方法提供內存空間。有些虛擬機的實現直接把本地方法棧和虛擬機棧合二爲一,比如主流的HotSpot虛擬機。

異常(Exception)Java虛擬機規範規定該區域可拋出StackOverFlowErrorOutOfMemoryError

2.4 Java

Java堆,是Java虛擬機管理的最大的一塊內存,也是GC的主戰場,所以可以叫它gc(垃圾堆),裏面存放的是幾乎所有的對象實例和數組數據。

異常(Exception)Java虛擬機規範規定該區域可拋出OutOfMemoryError

2.5 方法區

方法區主要存放的是已被虛擬機加載的類信息、常量、靜態變量、編譯器編譯後的代碼等數據。Java虛擬機規範對這一塊區域的限制非常寬鬆,不同的虛擬機實現也不同,相對而言垃圾回收在這個區域比較少的出現。根據java虛擬機規範,當方法區無法滿足內存分配需求時,會拋出oom異常。

2.6 運行常量池

運行時常量池是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。運行時常量池除了編譯期產生的Class文件的常量池,還可以在運行期間,將新的常量加入常量池,比較String類的intern()方法。

  • 字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明爲final的常量值等。
  • 符號引用:編譯語言層面的概念,包括以下3類:
    • 類和接口的全限定名
    • 字段的名稱和描述符
    • 方法的名稱和描述符

屬於方法區一部分,所以和方法區一樣,會oom

 

局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。

——因爲它們屬於方法中的變量,生命週期隨方法而結束。

成員變量全部存儲與堆中(包括基本數據類型,引用和引用的對象實體)

——因爲它們屬於類,類對象終究是要被new出來使用的。

 

們說的內存泄露,是針對,也只針對堆內存,他存放的就是引用指向的體。

 

內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage CollectionGC)完成的,java程序員不需要通過調用函數來釋放內存,但gc只能回收無用並且不再被其它對象引用的那些對象所佔用的空間。

 

堆中幾乎存放着Java世界中所有的例,垃圾收集器在堆回收之前,第一件事情就是要確定象哪些存活着,哪些象已死去(即不可能再被任何途徑使用的)

 

確定象是否活着的方法有:

1、引用數算法

       1.1算法分析 

引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,且將該對象實例分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1a = b,b引用的對象實例的計數器+1),當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1

1.2優缺點

優點:

  引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。

缺點:

  無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0.

1引用計數算法無法解決循環引用問題,例如:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
        
        object1.object = object2;
        object2.object = object1;
        
        object1 = null;
        object2 = null;
    }
}

最後面兩句將object1object2賦值爲null,也就是說object1object2指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不爲0,那麼垃圾收集器就永遠不會回收它們。

 

 

2、可達性分析算法(主流方法)

 

  可達性分析算法中,通過一系列的gc root爲起始點,從一個GC ROOT開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點。

java中可作爲GC Root的對象有

  1.虛擬機棧(本地變量表)中正在運行使用的引用

  2.方法區中靜態屬性引用的對象

  3. 方法區中常量引用的對象

4.本地方法棧JNI中引用的對象(Native對象)

上圖中objDobjEGC ROOT不可達,所以可以被回收。而其他的對gc root可達。

在代碼看來,類似於:(GC/Main.java

 

但是即使在可達性分析算法中不可達的對象,也並非一定要死。當gc第一次掃過這些對象的時候,他們處於“死緩”的階段。要真正執行死刑,至少需要經過兩次標記過程。如果對象經過可達性分析之後發現沒有與GC Roots相關聯的引用鏈,那他會被第一次標記,並經歷一次篩選。這個對象的finalize方法會被執行。如果對象沒有覆蓋finalize或者已經被執行過了。虛擬機也不會去執行finalize方法。Finalize是對象逃獄的最後一次機會。

Reference項目的FinalizeEscapeGC

 

輸出結果:

 

在例子中,對象第一次被執行了finalize方法,但是把自己上交給國家逃了一死,但是在給國家執行任務的時候,不幸犧牲了。所以沒辦法再自救了。

這個對象的finalize方法執行了一次(自救而不是被救,this賦值,所以給的還是自己

 

 

在說到內存的問題,我們都會提到一個關鍵詞:引用。

    通俗的,通A調用並訪問到B,那就明A持有B的引用,或A就是B的引用。

       比如 Person p1 = new Person();P1能操作Person對象,因此P1Person的引用;p1是O中的一個成員變量,因此我可以使用o.p1的方式來訪問Person類對象的成員,因此o持有一個Person對象的引用

       

       GC過程與對象的引用類型是密切相關的,

Java對引用的分類Strong reference(強), SoftReference(軟), WeakReference(), PhatomReference()

強引用就是在程序代碼中普遍存在的,比如”Object obj = new Object()”這種引用,只要強引用還在,垃圾收集器就不會回收被引用的對象。

軟引用用來定義一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要內存溢出之前,會將這些對象列入回收範圍進行第二次回收,如果回收後還是內存不足,纔會拋出內存溢出。

弱引用也是用來描述非必須對象。但他的強度比軟引用更弱一些。被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器回收時,無論內存是否足夠,都會回收掉被弱引用關聯的對象。

虛引用也稱爲幽靈引用或者幻影引用,是最弱的引用關係。一個對象的虛引用根本不影響其生存時間,也不能通過虛引用獲得一個對象實例。虛引用的唯一作用就是這個對象被GC時可以收到一條系統通知。

 

 

 

Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大而且生命週期較長候,可以儘量引用和弱引用技

 

對於軟引用和弱引用的選擇,

如果只是想避免OutOfMemory異常的生,可以使用引用。如果用的性能更在意,想盡快回收一些佔用內存比大的象,可以使用弱引用。另外可以根據象是否常使用來判斷選擇軟引用是弱引用。如果該對象可能會常使用的,就儘量用引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

 

內存泄漏就是

堆內存中的生命週期的象持有短生命週期象的引用,儘管短生命週期象已不再需要,但是因爲長生命週期象持有它的引用而致不能被回收,就是Java中內存泄露的根本原因。

 總結一句就是不需要了該回收因引用問題導致不能回收。

 

內存泄漏會致可用內存慢慢少,程序慢慢卡。最終還會導致臭名昭著的oom 內存溢出。

 

 

我們的android程序應該怎麼排查內存泄漏問題呢?

在android中我們執行一段代,比如入了一個新的面(Activity),這時候我的內存使用肯定比在前一個面大,而在界面finish返回後,如果內存沒有回落,那麼很有可能就是出了內存泄漏。

 

從內存監控工具中觀察內存曲線,是否存在不斷上升的趨勢且不會在程序返回時明顯回落。這種方式可以發現最基本,也是最明顯的內存泄露問題,對用戶價值最大,操作難度小,性價比極高。

因爲他能發現很明顯很嚴重的內存泄漏問題

 

可以通AS的Memory Profile或者DDMS中的heap察內存使用情況。

    

 

 

Android Profile

   https://developer.android.com/studio/preview/features/android-profiler.html

 

     The new Android Profiler window in Android Studio 3.0 replaces the Android Monitor tools.

 

       在Android Studio我們可以運行app。

然後我們能看到

 

我們點擊memory後

 

 

  • ① 強制執行垃圾收集事件的按鈕。
  • ② 捕獲堆轉儲的按鈕。
  • ③ 記錄內存分配的按鈕。
  • ④ 放大時間線的按鈕。
  • ⑤ 跳轉到實時內存數據的按鈕。
  • ⑥ 事件時間線顯示活動狀態、用戶輸入事件和屏幕旋轉事件。
  • ⑦ 內存使用時間表,其中包括以下內容: 
    • 每個內存類別使用多少內存的堆棧圖,如左邊的y軸和頂部的顏色鍵所示。
    • 虛線表示已分配對象的數量,如右側y軸所示。
    • 每個垃圾收集事件的圖標。

 

與之前的Android監控工具相比,新的內存分析器記錄了更多內存使用情況,所以看起來你的內存使用量會更高。內存分析器監視一些額外的類別,這些類別增加了總數。

 

 

在比較茫然的情況下,不知道哪兒出現了內存泄漏,我們可以進行一刀切,來個大致的排查。

 

我們進入了一大堆頁面並最終返回到主頁。

 

 

然後gc ,再dump下內存查看。AS可以點擊

然後等待一段時間會出現:

 

但是說實話這個頁面想要分析出什麼很難。一般不會使用這個頁面來進行分析,最多打開看一眼剛剛我們進入的Activity是否因爲我們退出而回收。

 

先按照包名來分組,

 

Alloc Cout : 對象數

Shallow Size : 對象佔用內存大小

Retained Set : 對象引用組佔用內存大小(包含了這個對象引用的其他對象)

 

當然一次dump可能並不能發現內存泄漏,可能每次我們dump的結果都不同,那麼就需要多試幾次,然後結合代碼來排查。

 

 

這裏還不能確定發生了內存泄漏。

我們這時候可以藉助一些更專業的工具來進行內存的分析。

我們先把這個內存快照保存爲hprof文件。

 

AS自動分析

其實現在的AS,可以說是非常強大了。我們把剛剛保存的hprof文件拖入到AS中。

 

 

 

這個自動分析任務包含了兩個內容,一個是檢測Activity的泄漏,一個是檢測重複字符串。

點擊運行分析:

 

這裏出現了MainActivity的泄漏。並且觀察到這個MainActivity可能不止一個對象存在,可能是我們上次退出程序的時候發生了泄漏,導致它不能回收。而在此打開app,系統會創建新的MainActivity。

但是在AS上發現爲何沒被回收需要運氣,更方便的工具是Mat。

 

 

Memory Analyzer Tool基於eclipse

可以直接下載:

http://www.eclipse.org/mat/downloads.php

也可以在eclispe上安裝mat插件:

 

點擊eclipse marketplace...搜索memory。

 

 

在使用mat之前我們需要把快照文件轉換一下,

轉換工具在sdk/platform-tools/hprof-conv 

-z:排除不是app的內存,比如Zygote

hprof-conv -z src dst

 

 

然後在Mat中打開它:

 

打開我們的快照文件,

 

之後我們能看到

 

 

我們點擊

 

以直方圖的方式來顯示當前內存使用情況可能更加適合較爲複雜的內存泄漏分析,它默認直接顯示當前內存中各種類型對象的數量及這些對象的shallow heap和retained heap。結合MAT提供的不同顯示方式,往往能夠直接定位問題

  • shallow heap:指的是某一個對象所佔內存大小。
  • retained heap:指的是一個對象與所包含對象所佔內存的總大小。

 

out查看這個對象持有的外部對象引用

incoming查看這個對象被哪些外部對象引用

 

我們現在希望查看爲什麼這個對象還存在,那麼

 

排除軟弱虛引用。

 

關於這個問題是android系統的一個bug。

原因是Activity的DecorView請求了InputMethodManager,而InputMethodManager一直持有DecorView的引用,導致無法釋放Activity。

解決辦法是:

 

 

 

 

這個問題是一個第三方庫中持有引用,導致的無法釋放。

這個問題可能是這個第三方庫本身的問題,也可能是我們使用不當引起的。爲了確定這個問題。我們進入被混淆了的g.e這個類,

注意這裏是g.e中的a成員是一個HashMap,

 

結合代碼

 

這裏面保存的都是一些Observer對象。這裏就需要結合對代碼的熟悉度,加上一些猜測來尋找問題。

在Activity中搜索Observer,可以找到很多個new Observer。

但是我們注意,調用的地方第二個參數是true,表示註冊;如果傳false表示註銷

 

而這裏只有註冊,沒有註銷。

 

我們在onDestory中加入註銷。

 

修改完成再次運行,然後退出app一次再打開:

結果:

 

還有兩個Activity。

第一個問題仍然是observer,但是是在MessageFragment。

第二個問題也還是InputMethodmanager,但是泄漏點變了。

修改:

 

 

內存泄漏解決完成!

    這個app還有很多內存泄漏的地方。大家可以自己嘗試去尋找並解決。比如MyInfoActivity。。。。大家自己去解決啊,有什麼不懂得再問我。如果問的人多,下節課先解決掉這個泄漏。

 

除了檢查單個hprof文件之外,還能夠使用多個hprof進行對比。

比如我們在進入一個新頁面之前先dump下來內存。然後再進入這個頁面之後退出,再dump一份內存。通過對比就能夠知道,進入這個頁面之後增加了多少內存/對象等信息:

 

之後在mat中打開這個兩個文件並都切換到直方圖查看。

再然後把兩個直方均加入對比,點擊,或者

 

執行對比:

 

再把視圖切換到difference from base table(與第一個的不同)

 

然後能看到

 

第二行就是指第二個文件相比第一個文件多出來了幾個對象。

如果存在增加了不合理的對象,同樣可以查看其GC root。

 

 

=====================================================================

 

Android內存泄露 們還可以使用著名的LeakCanary (Square出品,SquareAndroid開源界中的界良心,開源的目包括okhttp, retrofitotto, picasso, Android大神Jake Wharton曾今就是Square)檢測

https://github.com/square/leakcanary

這個庫也有一些bug,但總體來說還是能起到一定的輔助作用。

 

總結:

內存泄漏常見原因:

1.集合類

集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被佔用。如果這個集合類是全局性的變量(比如類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的內存只增不減。

       一開始的微信工程中使用的ButterKnife中的linkeahashmap就存在這個問題。

2、靜態成員

       Static成員作爲gc root,如果一個對象被static聲明,這個對象會一直存活直到程序進程停止。

2.單例模式

不正確使用單例模式是引起內存泄露的一個常見問題,單例對象在被初始化後將在 JVM 的整個生命週期中存在(以靜態變量的方式),如果單例對象持有外部對象的引用,那麼這個外部對象將不能被 JVM 正常回收,導致內存泄露。

       

這裏如果傳遞Activity作爲Context來獲得單例對象,那麼單例持有Activity的引用,導致Activity不能被釋放。

不要直接對 Activity 進行直接引用作爲成員變量,如果允許可以使用Application。如果不得不需要Activity作爲Context,可以使用弱引用WeakReference,相同的,對於Service 等其他有自己聲明週期的對象來說,直接引用都需要謹慎考慮是否會存在內存泄露的可能。

 

3.未關閉/釋放資源

BraodcastReceiverContentObserverFileObserverCursorCallback等在 Activity onDestroy 或者某類生命週期結束之後一定要 unregister 或者 close 掉,否則這個 Activity 類會被 system 強引用,不會被內存回收。

       我們經常會寫出下面的代碼

 

當然這樣寫代碼沒問題,但是如果我們在close之前還有一些可能拋出異常的代碼

 

那麼現在這段代碼存在隱患的。因爲如果運行到fos2時候拋出了異常,那麼fos也沒辦法close

所以正確的方式應該是

 

因爲如果write發生異常那麼這個fos會因爲沒有close造成內存泄漏。

4. Handler

只要 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。特別是handler執行延遲任務。所以,Handler 的使用要尤爲小心,否則將很容易導致內存泄露的發生。

 

這種創建Handler的方式會造成內存泄漏,由於mHandler是Handler的非靜態匿名內部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏,所以另外一種做法爲:

 

創建一個靜態Handler內部類,然後對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity泄漏,不過Looper線程的消息隊列中還是可能會有待處理的消息,所以我們在Activity的Destroy時或者Stop時應該移除消息隊列中的消息,

 

使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。當然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。

 

5. Thread 內存泄露

handler一樣,線程也是造成內存泄露的一個重要的源頭。線程產生內存泄露的主要原因在於線程生命週期的不可控。比如線程是 Activity 的內部類,則線程對象中保存了 Activity 的一個引用,當線程的 run 函數耗時較長沒有結束時,線程對象是不會被銷燬的,因此它所引用的老的 Activity 也不會被銷燬,因此就出現了內存泄露的問題。

ThreadHandler都可以劃分到爲非靜態包括匿名內部類的內存泄漏。

 

6、系統bug,比如InputMethodManager

 

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