我們常說(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ò)多的信息。
這里的參考資料有:
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è) Service。Activity 對(duì)象包括大量的布局和資源文件, 一旦它被該單例持有,它所持有的資源在應(yīng)用結(jié)束前都不會(huì)被釋放。修改的方法很簡(jiǎn)單:
private AccountMananger(Context context) {
if (context != null) {
mContext = context.getApplicationContext();
}
}
傳進(jìn)來(lái)的Context用ApplicationContext就可以了。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ì)被回收。
解決辦法就是在Activity的onDestroy方法中將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é):
- 使用靜態(tài)變量的時(shí)候要小心,尤其要注意
Activity/Service等大對(duì)象的傳值。在單例模式中能用ApplicationContext的都用ApplicationContext,或者把聚合關(guān)系改成依賴關(guān)系,不在單例對(duì)象中持有Context引用; - 養(yǎng)成良好的代碼習(xí)慣。注冊(cè)/反注冊(cè)要成對(duì)出現(xiàn),
Activity和Service對(duì)象中避免使用非靜態(tài)內(nèi)部類/匿名內(nèi)部類,除非十分清楚引用關(guān)系; - 使用多線程的時(shí)候留意線程存活時(shí)間。盡量將聚合關(guān)系改成依賴關(guān)系,減少線程對(duì)象持有大對(duì)象的時(shí)間;
- 在使用
xxxStream,SqlLiteDatabase,Cursor類的時(shí)候要注意釋放資源。使用Timer,TimerTask的時(shí)候要記得取消任務(wù)。Bitmap在使用結(jié)束后要記得recycler()。