Android開發從GC root分析內存泄漏


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

經過查閱,了解JVM中GC Roots的大致分類,然后用自己的語言解釋一下:

  • Class 由System Class Loader/Boot Class Loader加載的類對象,這些對象不會被回收。需要注意的是其它的Class Loader實例加載的類對象不一定是GC root,除非這個類對象恰好是其它形式的GC root;
  • Thread 線程,激活狀態的線程;
  • Stack Local 棧中的對象。每個線程都會分配一個棧,棧中的局部變量或者參數都是GC root,因為它們的引用隨時可能被用到;
  • JNI Local JNI中的局部變量和參數引用的對象;可能在JNI中定義的,也可能在虛擬機中定義
  • JNI Global JNI中的全局變量引用的對象;同上
  • Monitor Used 用於保證同步的對象,例如wait(),notify()中使用的對象、鎖等。
  • Held by JVM JVM持有的對象。JVM為了特殊用途保留的對象,它與JVM的具體實現有關。比如有System Class Loader, 一些Exceptions對象,和一些其它的Class Loader。對於這些類,JVM也沒有過多的信息。

這里的參考資料有:

Yourkit

What are the roots?
了解過GC Roots之后,可以幫助我們定位內存泄漏。因為被GC roots直接或者間接引用的對象都不會被回收,所以我們要確保我們用的局部對象遠離這些危險的類。下面根據GC root的分類分析一下幾種內存泄漏的原因。

1. Class


應用運行過程中非動態加載的類都是通過dalvik.system.PathClassLoader的實例加載到虛擬機中的。這些類對象是GC root的一種,它們帶來的靜態變量永遠不會被垃圾回收。因此,靜態變量持有的“過期”對象將會造成內存泄漏。下面舉幾個例子。

單例:

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;
    }
}

上面這段代碼就很危險,因為單例對象持有一個 Context。它可能是一個 Activity 也可能是一個 ServiceActivity 對象包括大量的布局和資源文件, 一旦它被該單例持有,它所持有的資源在應用結束前都不會被釋放。修改的方法很簡單:

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

傳進來的ContextApplicationContext就可以了。ApplicationContext對象在應用整個生命周期中有且只有一個對象。持有它的引用不會占用更多資源。

注冊/反注冊

public class AccountMananger extends Observable{
//單例的內容
}

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
    }
    
    
}

上面的代碼也會導致內存泄漏,因為注冊了監聽模式卻沒有反注冊。注冊過的監聽者都會間接的被單例對象持有,他們都不會被GC回收。修改方法:

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

所有的注冊型的用法都要有反注冊。編碼的時候養成好習慣,像Activity,Fragment等類在生命周期對等的回調方法中,最好成對的添加代碼。例如在onCreate()方法注冊監聽之后,馬上在onDestroy()方法中反注冊。

非靜態內部類/匿名類 + 靜態變量

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);

        }
    }
}

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

2. Thread


Runnable/AsyncTask

激活狀態的線程是不會被GC回收的,所以它持有的對象也不會被回收。看下面的代碼:

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) ;
            //正常情況下,線程執行時間不會無限,但可能有5分鍾,10分鍾
        }
    }
}

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

隱藏的線程

比如說在一個Activity中實現一個電子鍾:

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;
                ...
            }
        }
    }

每一個Timer類都運行在一個獨立的線程中。例子中我們的Timer對象的線程被設置為1000ms觸發一次操作,永不結束。需要注意的是當前的引用關系Timer->TimerTask->Activity。所以當我們的Activity結束之后,還會被GC root間接持有。這個Activity每次被打開都會多一個對象在進程中,並且永遠不會被回收。
解決辦法就是在ActivityonDestroy方法中將Timer取消掉。

3. JNI Local & JNI Global


這類對象一般發生在參與Jni交互的類中。

比如說很多close()相關的類,InputStream,OutputStream,Cursor,SqliteDatabase等。這些對象不止被Java代碼中的引用持有,也會被虛擬機中的底層代碼持有。在將持有它們的引用設置為null之前,要先將他們close()掉。
還有一個特殊的類是Bitmap。在Android系統3.0之前,它的內存一部分在虛擬機中,一部分在虛擬機外。因此它的一部分內存不參與垃圾回收,需要我們主動調用recycler()才能回收。

動態鏈接庫中的內存是用C/C++語言申請的,這些內存不受虛擬機的管轄。所以,so庫中的數組,類等都有可能發生內存泄漏,使用的時候務必小心。

總結:


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

參考文章:
Android 內存泄漏總結
Android內存泄漏分析及調試


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM