Android開發(fā)從GC root分析內(nèi)存泄漏

我們常說(shuō)的垃圾回收機(jī)制中會(huì)提到GC Roots這個(gè)詞,也就是Java虛擬機(jī)中所有引用的根對(duì)象。我們都知道,垃圾回收器不會(huì)回收GC Roots以及那些被它們間接引用的對(duì)象。但是,對(duì)于GC Roots的定義卻不是很清楚。它們都包括哪些對(duì)象呢?

經(jīng)過(guò)查閱,了解JVM中GC Roots的大致分類,然后用自己的語(yǔ)言解釋一下:

  • Class 由System Class Loader/Boot Class Loader加載的類對(duì)象,這些對(duì)象不會(huì)被回收。需要注意的是其它的Class Loader實(shí)例加載的類對(duì)象不一定是GC root,除非這個(gè)類對(duì)象恰好是其它形式的GC root;
  • Thread 線程,激活狀態(tài)的線程;
  • Stack Local 棧中的對(duì)象。每個(gè)線程都會(huì)分配一個(gè)棧,棧中的局部變量或者參數(shù)都是GC root,因?yàn)樗鼈兊囊秒S時(shí)可能被用到;
  • JNI Local JNI中的局部變量和參數(shù)引用的對(duì)象;可能在JNI中定義的,也可能在虛擬機(jī)中定義
  • JNI Global JNI中的全局變量引用的對(duì)象;同上
  • Monitor Used 用于保證同步的對(duì)象,例如wait(),notify()中使用的對(duì)象、鎖等。
  • Held by JVM JVM持有的對(duì)象。JVM為了特殊用途保留的對(duì)象,它與JVM的具體實(shí)現(xiàn)有關(guān)。比如有System Class Loader, 一些Exceptions對(duì)象,和一些其它的Class Loader。對(duì)于這些類,JVM也沒(méi)有過(guò)多的信息。

這里的參考資料有:

Yourkit

What are the roots?
了解過(guò)GC Roots之后,可以幫助我們定位內(nèi)存泄漏。因?yàn)楸籊C roots直接或者間接引用的對(duì)象都不會(huì)被回收,所以我們要確保我們用的局部對(duì)象遠(yuǎn)離這些危險(xiǎn)的類。下面根據(jù)GC root的分類分析一下幾種內(nèi)存泄漏的原因。

1. Class


應(yīng)用運(yùn)行過(guò)程中非動(dòng)態(tài)加載的類都是通過(guò)dalvik.system.PathClassLoader的實(shí)例加載到虛擬機(jī)中的。這些類對(duì)象是GC root的一種,它們帶來(lái)的靜態(tài)變量永遠(yuǎn)不會(huì)被垃圾回收。因此,靜態(tài)變量持有的“過(guò)期”對(duì)象將會(huì)造成內(nèi)存泄漏。下面舉幾個(gè)例子。

單例:

public class AccountMananger {
    private Context mContext;
    private static AccountMananger instance = null;
    
    public static AccountMananger getInstance(Context context) {
        if (instance == null) {
            synchronized (AccountManager.class) {
                if (instance == null) {
                    instance = new AccountMananger(context);
                }
            }
        }
        return instance;
    }

    private AccountMananger(Context context) {
        mContext = context;
    }
}

上面這段代碼就很危險(xiǎn),因?yàn)閱卫龑?duì)象持有一個(gè) Context。它可能是一個(gè) Activity 也可能是一個(gè) ServiceActivity 對(duì)象包括大量的布局和資源文件, 一旦它被該單例持有,它所持有的資源在應(yīng)用結(jié)束前都不會(huì)被釋放。修改的方法很簡(jiǎn)單:

    private AccountMananger(Context context) {
        if (context != null) {
            mContext = context.getApplicationContext();
        }
    }

傳進(jìn)來(lái)的ContextApplicationContext就可以了。ApplicationContext對(duì)象在應(yīng)用整個(gè)生命周期中有且只有一個(gè)對(duì)象。持有它的引用不會(huì)占用更多資源。

注冊(cè)/反注冊(cè)

public class AccountMananger extends Observable{
//單例的內(nèi)容
}

public class MainActivity extends AppCompatActivity implements Observer {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AccountMananger.getInstance(this).addObserver(this);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
    
    ...
    
    @Override
    public void update(Observable observable, Object data) {
        //todo Your logic
    }
    
    
}

上面的代碼也會(huì)導(dǎo)致內(nèi)存泄漏,因?yàn)樽?cè)了監(jiān)聽模式卻沒(méi)有反注冊(cè)。注冊(cè)過(guò)的監(jiān)聽者都會(huì)間接的被單例對(duì)象持有,他們都不會(huì)被GC回收。修改方法:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        AccountMananger.getInstance(this).deleteObserver(this);
    }

所有的注冊(cè)型的用法都要有反注冊(cè)。編碼的時(shí)候養(yǎng)成好習(xí)慣,像Activity,Fragment等類在生命周期對(duì)等的回調(diào)方法中,最好成對(duì)的添加代碼。例如在onCreate()方法注冊(cè)監(jiān)聽之后,馬上在onDestroy()方法中反注冊(cè)。

非靜態(tài)內(nèi)部類/匿名類 + 靜態(tài)變量

public class MainActivity extends AppCompatActivity {
    private static MyHandler handler = new MyHandler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    public class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

        }
    }
}

非靜態(tài)內(nèi)部類會(huì)持有外部類的引用(所以它才可以直接訪問(wèn)外部類的成員變量)。上面代碼中的靜態(tài)handler變量間接持有了MainActivity對(duì)象。這樣就造成了內(nèi)存泄漏。
解決的方法就是將內(nèi)部類中對(duì)外部類的調(diào)用改成public方法,然后將Handler改成靜態(tài)內(nèi)部類或者外部一個(gè)類。或者將將它放到弱引用中。

2. Thread


Runnable/AsyncTask

激活狀態(tài)的線程是不會(huì)被GC回收的,所以它持有的對(duì)象也不會(huì)被回收。看下面的代碼:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AsyncTasks asyncWork = new AsyncTasks(this);
        ExecutorService defaultExecutor = Executors.newCachedThreadPool();
        defaultExecutor.execute(asyncWork);
    }

    public static class AsyncTasks implements Runnable {
        private Context context;

        public AsyncTasks(Context context) {
            this.context = context;
        }

        @Override
        public void run() {
            while (true) ;
            //正常情況下,線程執(zhí)行時(shí)間不會(huì)無(wú)限,但可能有5分鐘,10分鐘
        }
    }
}

線程中持有一個(gè)Activity對(duì)象,在這個(gè)線程活躍的時(shí)間內(nèi)這個(gè)Activity對(duì)象都不會(huì)被釋放。因此,其它線程中盡量不要持有Activity,Service等大對(duì)象。如果需要用到Context,盡量使用ApplicationContext。

隱藏的線程

比如說(shuō)在一個(gè)Activity中實(shí)現(xiàn)一個(gè)電子鐘:

public class MainActivity extends AppCompatActivity {
    private TextView tvClock = null;
    Timer clock = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvClock=findViewById(R.id.tv_clock);
        TimerTask clockTask = new TimerTask() {
            @Override
            public void run() {
                tvClock.setText(updateClockText());
            }
        };
        clock = new Timer();
        clock.schedule(clockTask, 0, 1000);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

Timer的部分源碼如下:

public class Timer {

    private static final class TimerImpl extends Thread {
    ....
        /**
         * This method will be launched on separate thread for each Timer
         * object.
         */
        @Override
        public void run() {
            while (true) {
                TimerTask task;
                ...
            }
        }
    }

每一個(gè)Timer類都運(yùn)行在一個(gè)獨(dú)立的線程中。例子中我們的Timer對(duì)象的線程被設(shè)置為1000ms觸發(fā)一次操作,永不結(jié)束。需要注意的是當(dāng)前的引用關(guān)系Timer->TimerTask->Activity。所以當(dāng)我們的Activity結(jié)束之后,還會(huì)被GC root間接持有。這個(gè)Activity每次被打開都會(huì)多一個(gè)對(duì)象在進(jìn)程中,并且永遠(yuǎn)不會(huì)被回收。
解決辦法就是在ActivityonDestroy方法中將Timer取消掉。

3. JNI Local & JNI Global


這類對(duì)象一般發(fā)生在參與Jni交互的類中。

比如說(shuō)很多close()相關(guān)的類,InputStream,OutputStream,Cursor,SqliteDatabase等。這些對(duì)象不止被Java代碼中的引用持有,也會(huì)被虛擬機(jī)中的底層代碼持有。在將持有它們的引用設(shè)置為null之前,要先將他們close()掉。
還有一個(gè)特殊的類是Bitmap。在Android系統(tǒng)3.0之前,它的內(nèi)存一部分在虛擬機(jī)中,一部分在虛擬機(jī)外。因此它的一部分內(nèi)存不參與垃圾回收,需要我們主動(dòng)調(diào)用recycler()才能回收。

動(dòng)態(tài)鏈接庫(kù)中的內(nèi)存是用C/C++語(yǔ)言申請(qǐng)的,這些內(nèi)存不受虛擬機(jī)的管轄。所以,so庫(kù)中的數(shù)組,類等都有可能發(fā)生內(nèi)存泄漏,使用的時(shí)候務(wù)必小心。

總結(jié):


  1. 使用靜態(tài)變量的時(shí)候要小心,尤其要注意Activity/Service等大對(duì)象的傳值。在單例模式中能用ApplicationContext的都用ApplicationContext,或者把聚合關(guān)系改成依賴關(guān)系,不在單例對(duì)象中持有Context引用;
  2. 養(yǎng)成良好的代碼習(xí)慣。注冊(cè)/反注冊(cè)要成對(duì)出現(xiàn),ActivityService對(duì)象中避免使用非靜態(tài)內(nèi)部類/匿名內(nèi)部類,除非十分清楚引用關(guān)系;
  3. 使用多線程的時(shí)候留意線程存活時(shí)間。盡量將聚合關(guān)系改成依賴關(guān)系,減少線程對(duì)象持有大對(duì)象的時(shí)間;
  4. 在使用xxxStream,SqlLiteDatabase,Cursor類的時(shí)候要注意釋放資源。使用Timer,TimerTask的時(shí)候要記得取消任務(wù)。Bitmap在使用結(jié)束后要記得recycler()

參考文章:
Android 內(nèi)存泄漏總結(jié)
Android內(nèi)存泄漏分析及調(diào)試

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容