中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

系統(tǒng)剖析 Android 中的內(nèi)存泄漏

2018-07-20    來源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢上線!快速搭建集群,上萬Linux鏡像隨意使用

作為Android開發(fā)人員,我們或多或少都聽說過內(nèi)存泄漏。那么何為內(nèi)存泄漏,Android中的內(nèi)存泄漏又是什么樣子的呢,本文將簡單概括的進(jìn)行一些總結(jié)。

關(guān)于內(nèi)存泄露的定義,我可以理解成這樣

沒有用的對象無法回收的現(xiàn)象就是內(nèi)存泄露

如果程序發(fā)生了內(nèi)存泄露,則會帶來如下的問題

  • 應(yīng)用可用的內(nèi)存減少,增加了堆內(nèi)存的壓力
  • 降低了應(yīng)用的性能,比如會觸犯更頻繁的GC
  • 嚴(yán)重的時(shí)候可能會導(dǎo)致內(nèi)存溢出錯誤,即OOM Error

在正式介紹內(nèi)存泄露之前,我們有必要介紹一些必要的預(yù)備知識。

預(yù)備知識1: Java中的對象

  • 當(dāng)我們使用 new 指令生成對象時(shí),堆內(nèi)存將會為此開辟一份空間存放該對象
  • 創(chuàng)建的對象可以被局部變量,實(shí)例變量和類變量引用。
  • 通常情況下,類變量持有的對象生命周期最長,實(shí)例變量次之,局部變量最短。
  • 垃圾回收器回收非存活的對象,并釋放對應(yīng)的內(nèi)存空間。

預(yù)備知識2:Java中的GC

  • 和C++不同,對象的釋放不需要手動完成,而是由垃圾回收器自動完成。
  • 垃圾回收器運(yùn)行在JVM中
  • 通常GC有兩種算法:引用計(jì)數(shù)和GC根節(jié)點(diǎn)遍歷

引用計(jì)數(shù)

  • 每個(gè)對象有對應(yīng)的引用計(jì)數(shù)器
  • 當(dāng)一個(gè)對象被引用(被復(fù)制給變量,傳入方法中),引用計(jì)數(shù)器加1
  • 當(dāng)一個(gè)對象不被引用(離開變量作用域),引用計(jì)數(shù)器就會減1
  • 基于這種算法的垃圾回收器效率較高
  • 循環(huán)引用的問題引用計(jì)數(shù)算法的垃圾回收器無法解決。
  • 主流的JVM很少使用基于這種算法的垃圾回收器實(shí)現(xiàn)。

GC根節(jié)點(diǎn)遍歷

  • 識別對象為垃圾從被稱為GC 根節(jié)點(diǎn)出發(fā)
  • 每一個(gè)被遍歷的強(qiáng)引用可到達(dá)對象,都會被標(biāo)記為存活
  • 在遍歷結(jié)束后,沒有被標(biāo)記為存活的對象都被視為垃圾,需要后續(xù)進(jìn)行回收處理
  • 主流的JVM一般都采用這種算法的垃圾回收器實(shí)現(xiàn)

以上圖為例,我們可以知道

  • 最下層的兩個(gè)節(jié)點(diǎn)為GC Roots,即GC Tracing的起點(diǎn)
  • 中間的一層的對象,可以強(qiáng)引用到達(dá)GC根節(jié)點(diǎn),所以被標(biāo)記為存活
  • 最上層的三個(gè)對象,無法強(qiáng)引用達(dá)到GC根節(jié)點(diǎn),所以無法標(biāo)記為存活,也就是所謂的垃圾,需要被后續(xù)回收掉。

上面的垃圾回收中,我們提到的兩個(gè)概念,一個(gè)是GC根節(jié)點(diǎn),另一個(gè)是強(qiáng)引用

在Java中,可以作為GC 根節(jié)點(diǎn)的有

  • 類,由系統(tǒng)類加載器加載的類。這些類從不會被卸載,它們可以通過靜態(tài)屬性的方式持有對象的引用。注意,一般情況下由自定義的類加載器加載的類不能成為GC Roots
  • 線程,存活的線程
  • Java方法棧中的局部變量或者參數(shù)
  • JNI方法棧中的局部變量或者參數(shù)
  • JNI全局引用
  • 用做同步監(jiān)控的對象
  • 被JVM持有的對象,這些對象由于特殊的目的不被GC回收。這些對象可能是系統(tǒng)的類加載器,一些重要的異常處理類,一些為處理異常預(yù)留的對象,以及一些正在執(zhí)行類加載的自定義的類加載器。但是具體有哪些前面提到的對象依賴于具體的JVM實(shí)現(xiàn)。

提到強(qiáng)引用,有必要系統(tǒng)說一下Java中的引用類型。Java中的引用類型可以分為一下四種:

  • 強(qiáng)引用: 默認(rèn)的引用類型,例如 StringBuffer buffer = new StringBuffer(); 就是buffer變量持有的為StringBuilder的強(qiáng)引用類型。
  • 軟引用:即SoftReference,其指向的對象只有在內(nèi)存不足的時(shí)候進(jìn)行回收。
  • 弱引用:即WeakReference,其指向的對象在GC執(zhí)行時(shí)會被回收。
  • 虛引用:即PhantomReference,與ReferenceQueue結(jié)合,用作記錄該引用指向的對象已被銷毀。

補(bǔ)充了預(yù)備知識,我們就需要具體講一講Android中的內(nèi)存泄漏了。

Android中的內(nèi)存泄漏

歸納而言,Android中的內(nèi)存泄漏有以下幾個(gè)特點(diǎn):

  • 相對而言,Android中的內(nèi)存泄漏更加容易出現(xiàn)。
  • 由于Android系統(tǒng)為每個(gè)App分配的內(nèi)存空間有限,在一個(gè)內(nèi)存泄漏嚴(yán)重的App中,很容易導(dǎo)致OOM,即內(nèi)存溢出錯誤。
  • 內(nèi)存泄漏會隨著App的推出而消失(即進(jìn)程結(jié)束)。

在Android中的內(nèi)存泄漏場景有很多,按照類型劃分可以歸納為

  • 長期持有(Activity)Context導(dǎo)致的
  • 忘記注銷監(jiān)聽器或者觀察者
  • 由非靜態(tài)內(nèi)部類導(dǎo)致的

此外,如果按照泄漏的程度,可以分為

  • 長時(shí)間泄漏,即泄漏只能等待進(jìn)程退出才消失
  • 短時(shí)間泄漏,被泄漏的對象后續(xù)會被回收掉。

長時(shí)間持有Activity實(shí)例

在Android中,Activity是我們常用的組件,通常情況下,一個(gè)Activity會包含了一些復(fù)雜的UI視圖,而視圖中如果含有ImageView,則有可能會使用比較大的Bitmap對象。因而一個(gè)Activity持有的內(nèi)存會相對很多,如果造成了Activity的泄漏,勢必造成一大塊內(nèi)存無法回收,發(fā)生泄漏。

這里舉個(gè)簡單的例子,說明Activity的內(nèi)存泄漏,比如我們有一個(gè)叫做AppSettings的類,它是一個(gè)單例模式的應(yīng)用。

public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
      return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context;
    }
}

當(dāng)我們傳入Activity作為Context參數(shù)時(shí),則AppSettings實(shí)例會持有這個(gè)Activity的實(shí)例。

當(dāng)我們旋轉(zhuǎn)設(shè)備時(shí),Android系統(tǒng)會銷毀當(dāng)前的Activity,創(chuàng)建新的Activity來加載合適的布局。如果出現(xiàn)Activity被單例實(shí)例持有,那么旋轉(zhuǎn)過程中的舊Activity無法被銷毀掉。就發(fā)生了我們所說的內(nèi)存泄漏。

想要解決這個(gè)問題也不難,那就是使用Application的Context對象,因?yàn)樗虯ppSettings實(shí)例具有相同的生命周期。這里是通過使用 Context.getApplicationContext() 方法來實(shí)現(xiàn)。所以修改如下

public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
      return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context.getApplicationContext();
    }
}

忘記反注冊監(jiān)聽器

在Android中我們會使用很多l(xiāng)istener,observer。這些都是作為觀察者模式的實(shí)現(xiàn)。當(dāng)我們注冊一個(gè)listener時(shí),這個(gè)listener的實(shí)例會被主題所引用。如果主題的生命周期要明顯大于listener,那么就有可能發(fā)生內(nèi)存泄漏。

以下面的代碼為例

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }
}

上述代碼處理的業(yè)務(wù),可以理解為

  • AppCompatActivity實(shí)現(xiàn)了OnNetworkChangedListener接口,用來監(jiān)聽網(wǎng)絡(luò)的可用性變化
  • NetworkManager為單例模式實(shí)現(xiàn),其registerListener接收了MainActivity實(shí)例

又是單例模式,可知NetworkManager會持有MainActivity的實(shí)例引用,因而屏幕旋轉(zhuǎn)時(shí),MainActivity同樣無法被回收,進(jìn)而造成了內(nèi)存泄漏。

對于這種類型的內(nèi)存泄漏,解決方法是這樣的。即在MainActivity的onDestroy方法中加入反注銷的方法調(diào)用。

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        NetworkManager.getInstance().unregisterListener(this);
    }

}

非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄漏

在Java中,非靜態(tài)內(nèi)部類會隱式持有外部類的實(shí)例引用。想要了解更多,可以參考這篇文章 細(xì)話Java:”失效”的private修飾符

通常情況下,我們會書寫類似這樣的代碼

public class SensorListenerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
        sensorManager.registerListener(new SensorListener() {
            @Override
            public void onSensorChanged(int sensor, float[] values) {

            }

            @Override
            public void onAccuracyChanged(int sensor, int accuracy) {

            }
        }, SensorManager.SENSOR_ALL);
    }
}

其中上面的SensorListner實(shí)例是一個(gè)匿名內(nèi)部類的實(shí)例,也是非靜態(tài)內(nèi)部類的一種。因此SensorListner也會持有外部SensorListenerActivity的實(shí)例引用。

而SensorManager作為單例模式實(shí)現(xiàn),其生命周期與Application相同,和SensorListner對象生命周期不同,有可能間接導(dǎo)致SensorListenerActivity發(fā)生內(nèi)存泄漏。

解決這種問題的方法可以是

  • 使用實(shí)例變量存儲SensonListener實(shí)例,在Activity的onDestroy方法進(jìn)行反注冊。
  • 如果registerListener方法可以修改,可以使用弱引用或者WeakHashMap來解決。

除了上面的三種場景外,Android的內(nèi)存泄漏還有可能出現(xiàn)在以下情況

  • 使用 Activity.getSystemService() 使用不當(dāng),也會導(dǎo)致內(nèi)存泄漏。
  • 資源未關(guān)閉也會造成內(nèi)存泄漏
  • Handler使用不當(dāng)也可以造成內(nèi)存泄漏的發(fā)生
  • 延遲的任務(wù)也可能導(dǎo)致內(nèi)存泄漏

解決內(nèi)存泄漏

想要解決內(nèi)存泄漏無非如下兩種方法

  • 手動解除不必要的強(qiáng)引用關(guān)系
  • 使用弱引用或者軟引用替換強(qiáng)引用關(guān)系

下面會簡單介紹一些內(nèi)存泄漏檢測和解決的工具

Strictmode

  • StrictMode,嚴(yán)格模式,是Android中的一種檢測VM和線程違例的工具。
  • 使用 detectAll() 或者 detectActivityLeaks() 可以檢測Activity的內(nèi)存泄漏
  • 使用 setClassInstanceLimit() 可以限定類的實(shí)例個(gè)數(shù),可以輔助判斷某些類是否發(fā)生了內(nèi)存泄漏
  • 但是StrictMode只能檢測出現(xiàn)象,并不能提供更多具體的信息。
  • 了解更多關(guān)于StrictMode,請?jiān)L問 Android性能調(diào)優(yōu)利器StrictMode

Android Memory Monitors

Android Memory Monitor內(nèi)置于Android Studio中,用于展示應(yīng)用內(nèi)存的使用和釋放情況。它大致長成這個(gè)樣子

當(dāng)你的App占用的內(nèi)存持續(xù)增加,而且你同時(shí)出發(fā)GC,也沒有進(jìn)行釋放,那么你的App很有可能發(fā)生了內(nèi)存泄漏問題。

LeakCanary

  • LeakCanary是一個(gè)檢測Java和Android內(nèi)存泄漏的庫
  • 由Square公司開發(fā)
  • 集成LeakCanary之后,只需要等待內(nèi)存泄漏出現(xiàn)就可以了,無需認(rèn)為進(jìn)行主動檢測。
  • 關(guān)于如何使用LeakCanary,可以參考這篇文章 Android內(nèi)存泄漏檢測利器:LeakCanary

Heap Dump

  • 一個(gè)Heap dump就是某一時(shí)間點(diǎn)的內(nèi)存快照
  • 它包含了某個(gè)時(shí)間點(diǎn)的Java對象和類信息。
  • 我們可以通上述提到的Android Heap Monitor進(jìn)行Heap Dump,當(dāng)然LeakCanary也會生成Heap Dump文件。
  • 生成的Heap Dump文件擴(kuò)展名為.hprof 即Heap Profile.
  • 通常情況下,一個(gè)heap profile需要轉(zhuǎn)換后才能被MAT使用分析。

Shallow Heap VS Retained Heap

  • Shallow Heap 指的是對象自身的占用的內(nèi)存大小。
  • 對象x的Retained Set指的是如果對象x被GC移除,可以釋放總的對象的集合。
  • 對象x的Retained Heap指的就是上述x的Retained Set的占用內(nèi)存大小。

以上圖做個(gè)例子,進(jìn)行分析

  • A,B,C,D四個(gè)對象的Shallow Heap均為1M
  • B,C,D的Retained Heap均為1M
  • A的Retained Heap為4M

真實(shí)情況下如何計(jì)算泄漏內(nèi)存大小

上述的Retained Heap的大小獲取是基于假設(shè)的,而現(xiàn)實(shí)在進(jìn)行分析中不可能基于這種方法,那么實(shí)際上計(jì)算泄漏內(nèi)存的大小的方法其實(shí)是這樣的。

這里我們需要一個(gè)概念,就是Dominator Tree(統(tǒng)治者樹)。

  • 如果對象x統(tǒng)治對象y,那么每條從GC根節(jié)點(diǎn)到y(tǒng)對象的路徑都會經(jīng)過x,即x是GC根節(jié)點(diǎn)到y(tǒng)的必經(jīng)之路。
  • 上述情況下,我們可以說x是y的統(tǒng)治者
  • 最近統(tǒng)治者指的是離對象y最近的統(tǒng)治者。

上圖中

  • A和B都不無法統(tǒng)治C對象,即C對象被A和B的父對象統(tǒng)治
  • H不受F,G,D,E統(tǒng)治,但是受C統(tǒng)治
  • F和D是循環(huán)引用,但是按照路徑的方向(從根節(jié)點(diǎn)到對象),D統(tǒng)治F

內(nèi)存泄漏與OOM

  • OOM全稱Out Of Memory Error 內(nèi)存溢出錯誤
  • OOM發(fā)生在,當(dāng)我們嘗試進(jìn)行創(chuàng)建對象,但是堆內(nèi)存無法通過GC釋放足夠的空間,堆內(nèi)存也無法在繼續(xù)增長,從而完成對象創(chuàng)建請求,所以發(fā)生了OOM
  • OOM發(fā)生很有可能是內(nèi)存泄漏導(dǎo)致
  • 但是并非所有的OOM都是由內(nèi)存泄漏引起
  • 內(nèi)存泄漏也并不一定引起OOM

聲明

  • 其中第一張圖片GC回收圖來自Patrick Dubroy在Google IO的演講Keynote
  • 最后一張Dorminator Tree來自MAT官方網(wǎng)站

一些鏈接

  • 垃圾回收器如何處理循環(huán)引用
  • 譯文:理解Java中的弱引用
  • Android中Handler引起的內(nèi)存泄露
  • 避免Android中Context引起的內(nèi)存泄露
  • Google為何這樣設(shè)計(jì)OnSharedPreferenceChangeListener
  • Keynote下載地址

最后的話

內(nèi)存泄漏在App中很常見,需要我們花時(shí)間去解決。

處理內(nèi)存泄漏問題,不僅要解決掉,更應(yīng)該善于整理總結(jié),做到后續(xù)編碼中主動避免。

 

來自:http://droidyue.com/blog/2016/11/23/memory-leaks-in-android/

 

標(biāo)簽: Google 代碼 網(wǎng)絡(luò)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:APP啟動慢怎么辦,Android官方這樣說

下一篇:Go 語言的 10 個(gè)實(shí)用技巧