我們常說的垃圾回收機制中會提到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也沒有過多的信息。
這里的參考資料有:
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
也可能是一個 Service
。Activity
對象包括大量的布局和資源文件, 一旦它被該單例持有,它所持有的資源在應用結束前都不會被釋放。修改的方法很簡單:
private AccountMananger(Context context) {
if (context != null) {
mContext = context.getApplicationContext();
}
}
傳進來的Context
用ApplicationContext
就可以了。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
每次被打開都會多一個對象在進程中,並且永遠不會被回收。
解決辦法就是在Activity
的onDestroy
方法中將Timer
取消掉。
3. JNI Local & JNI Global
這類對象一般發生在參與Jni交互的類中。
比如說很多close()相關的類,InputStream
,OutputStream
,Cursor
,SqliteDatabase
等。這些對象不止被Java代碼中的引用持有,也會被虛擬機中的底層代碼持有。在將持有它們的引用設置為null之前,要先將他們close()
掉。
還有一個特殊的類是Bitmap
。在Android系統3.0之前,它的內存一部分在虛擬機中,一部分在虛擬機外。因此它的一部分內存不參與垃圾回收,需要我們主動調用recycler()
才能回收。
動態鏈接庫中的內存是用C/C++語言申請的,這些內存不受虛擬機的管轄。所以,so庫中的數組,類等都有可能發生內存泄漏,使用的時候務必小心。
總結:
- 使用靜態變量的時候要小心,尤其要注意
Activity/Service
等大對象的傳值。在單例模式中能用ApplicationContext
的都用ApplicationContext
,或者把聚合關系改成依賴關系,不在單例對象中持有Context
引用; - 養成良好的代碼習慣。注冊/反注冊要成對出現,
Activity
和Service
對象中避免使用非靜態內部類/匿名內部類,除非十分清楚引用關系; - 使用多線程的時候留意線程存活時間。盡量將聚合關系改成依賴關系,減少線程對象持有大對象的時間;
- 在使用
xxxStream
,SqlLiteDatabase
,Cursor
類的時候要注意釋放資源。使用Timer
,TimerTask
的時候要記得取消任務。Bitmap
在使用結束后要記得recycler()
。