1. 程式人生 > >Android 系統開發_內存泄漏篇 -- "內存泄漏"的前世今生

Android 系統開發_內存泄漏篇 -- "內存泄漏"的前世今生

tps ron n) 機制 observer sage 語言 per country

基礎了解

什麽是內存泄漏?

內存泄漏是當有程序不再使用到的內存時,釋放內存失敗而產生了無用的內存消耗。內存泄漏並不是指物理上的內存消失,這裏的內存泄漏是指由程序分配的內存,由於程序邏輯錯誤而導致程序失去了對該內存的控制,使得內存浪費。

Java 內存分配策略

Java 程序運行時的內存分配策略有三種,分別是?靜態分配?、?棧式分配?和?堆式分配?,對應的三種存儲策略使用的內存空間主要分別是?靜態存儲區(也稱方法區)?、?棧區?和?堆區?。

-?靜態存儲區(方法區):主要存放?靜態數據?、?全局 static 數據?和?常量。這塊內存在程序編譯時就已經分配好,並且在程序整個運行期間都存在。

-?棧區:當方法被執行時,方法體內的?局部變量

?(其中包括基礎數據類型、對象的引用)都在棧上創建,並在方法執行結束時這些局部變量所持有的內存將會自動被釋放。因為棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。

-??堆區: 又稱動態內存分配,通常就是指在程序運行時直接 new 出來的內存,也就是?對象的實例。這部分內存在不使用時將會由 Java 垃圾回收器(GC)來負責回收。

棧與堆的區別

在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內存空間,當超過該變量的作用域後,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。

堆內存用來存放所有由 new 創建的對象(包括該對象其中的所有成員變量)和數組。在堆中分配的內存,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數組。

舉例說明:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        // Sample 類的局部變量 s2 和引用變量 mSample2 都是存在於棧中,
        // 但 mSample2 指向的對象是存在於堆上
        Sample mSample2 = new Sample();
    }
}
// mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,
// 而它自己存在於棧中。
Sample mSample3 = new Sample();

Java是如何管理內存

Java的內存管理就是對象的分配和釋放問題。在 Java 中,程序員需要通過關鍵字 new 為每個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執行的。在 Java 中,內存的分配是由程序完成的,而內存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是 Java 程序運行速度較慢的原因之一。因為,GC 為了能夠正確釋放對象,GC 必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都需要進行監控。

監視對象狀態是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

Java中的內存泄漏

在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程序以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內存泄漏,這些對象不會被GC所回收,然而它卻占用內存。

在C++中,內存泄漏的範圍更大一些。有些對象被分配了內存空間,然後卻不可達,由於C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內存泄露。

通過分析,我們得知,對於C++,程序員需要自己管理邊和頂點,而對於Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。

因此,通過以上分析,我們知道在Java中也有內存泄漏,但範圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對於程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規範定義, 該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那麽我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。

以下給出一個 Java 內存泄漏的典型例子:

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

在這個例子中,我們循環申請Object對象,並將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那麽 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置為 null。

常見內存泄漏

永遠的單例

單例的使用在我們的程序中隨處可見,因為使用它可以完美的解決我們在程序中重復創建對象的問題,不過可別小瞧它。由於單例的靜態特性使得其生命周期跟應用的生命周期一樣長?,所以一旦使用有誤,小心無限制的持有Activity的引用而導致內存泄漏。

我們看個例子:

public class AppManager {

    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context;
    }

    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命周期的長短至關重要!?(實際常見)

1、如果此時傳入的是 Application 的 Context ,因為 Application 的生命周期就是整個應用的生命周期,所以這將沒有任何問題。

2、如果此時傳入的是 Activity 的 Context ,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例對象所持有,其生命周期等於整個應用程序的生命周期,所以當前 Activity 退出時它的內存並不會被回收,這就造成泄漏了。

正確的方式(寫法一):

public class AppManager {

    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context.getApplicationContext(); // 使用 Application 的 context
    }

    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

正確的方式(寫法二):

// 在你的 Application 中添加一個靜態方法,getContext() 返回 Application 的 context

...

context = getApplicationContext();

...
   /**
     * 獲取全局的context
     * @return 返回全局context對象
     */
    public static Context getContext(){
        return context;
    }

public class AppManager {

    private static AppManager instance;
    private Context context;

    private AppManager() {
        this.context = MyApplication.getContext(); // 使用Application 的context
    }

    public static AppManager getInstance() {
        if (instance == null) {
            instance = new AppManager();
        }
        return instance;
    }
}

靜態Activity

我們看一段代碼:

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;         // 這邊設置了靜態Activity,發生了內存泄漏
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

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

在上面代碼中,我們聲明了一個靜態的 Activity 變量並且在 TextView 的 OnClick 事件裏引用了當前正在運行的 Activity 實例,所以如果在 activity 的生命周期結束之前沒有清除這個引用,則會引起內存泄漏。因為聲明的 activity 是靜態的,會常駐內存,如果該對象不清除,則垃圾回收器無法回收變量。

我們可以這樣解決:

    protected void onDestroy() {
        super.onDestroy();
        activity = null;       // 在onDestory方法中將靜態變量activity置空,這樣垃圾回收器就可以將靜態變量回收
    }

靜態View

其實和靜態Activity頗為相似,我們看下代碼:

    ...
    private static View view;               // 定義靜態View
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

View一旦被加載到界面中將會持有一個Context對象的引用,在這個例子中,這個context對象是我們的Activity,聲明一個靜態變量引用這個View,也就引用了activity,所以當activity生命周期結束了,靜態View沒有清除掉,還持有activity的引用,因此內存泄漏了。

我們可以這樣解決:

protected void onDestroy() {
    super.onDestroy();
    view = null;         // 在onDestroy方法裏將靜態變量置空
} 

匿名類/AsyncTask

我們看下面的例子:

public class MainActivity extends AppCompatActivity {
    void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View aicButton = findViewById(R.id.at_button);
        aicButton.setOnClickListener(new View.OnClickListener() {
            @Override 
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }
}

上面代碼在activity中創建了一個匿名類 AsyncTask,匿名類和非靜態內部類相同,會持有外部類對象,這裏也就是activity,因此如果你在 Activity 裏聲明且實例化一個匿名的AsyncTask對象,則可能會發生內存泄漏,如果這個線程在Activity銷毀後還一直在後臺執行,那這個線程會繼續持有這個Activity的引用從而不會被GC回收,直到線程執行完成。

我們可以這樣解決:

自定義靜態 AsyncTask 類,並且讓 AsyncTask 的周期和 Activity 周期保持一致,也就是在 Activity 生命周期結束時要將 AsyncTask cancel 掉。

非靜態內部類

有的時候我們可能會在啟動頻繁的Activity中,為了避免重復創建相同的數據資源,可能會出現這種寫法:

public class MainActivity extends AppCompatActivity {

    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(mManager == null){
            mManager = new TestResource();
        }
        //...
    }

    class TestResource {
        //...
    }
}

上面這段代碼在Activity內部創建了一個非靜態內部類的單例(mManager),每次啟動Activity時都會使用該單例的數據,這樣雖然避免了資源的重復創建,不過這種寫法卻會造成內存泄漏。

因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收。

正確的做法為:

將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context。

Handler

Handler 的使用造成的內存泄漏問題應該說是?最為常見?了,很多時候我們為了避免 ANR 而不在主線程進行耗時操作,在處理網絡任務或者封裝一些請求回調等api都借助Handler來處理,但 Handler 不是萬能的,對於 Handler 的使用代碼編寫不規範即有可能造成內存泄漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。

由於 Handler 屬於 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實現方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導致無法正確釋放。

public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

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

        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        }, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

在該 SampleActivity 中聲明了一個延遲?10分鐘?執行的消息 Message,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 裏。當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主線程中,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內存泄漏(因 Handler 為非靜態內部類,它會持有外部類的引用,在這裏就是指 SampleActivity)。

正確的做法為:

在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 聲明為靜態的,則其存活期跟 Activity 的生命周期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,見下面代碼:

public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            SampleActivity activity = mActivity.get();
            if (activity != null) {                              // 每次使用前註意判空
                // ...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

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

        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

從上面的代碼中我們可以看出如何避免Handler內存泄漏,推薦使用 "靜態內部類 + WeakReference" 這種方式,每次使用前註意判空。

Java對引用的分類有Strong reference、SoftReference、WeakReference、PhatomReference四種。
技術分享圖片

在Android應用的開發中,為了防止內存溢出,在處理一些占用內存大而且聲明周期較長的對象時候,可以盡量應用軟引用和弱引用技術。

軟/弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用。

Thread

看個範例:

public class SampleActivity extends Activity {
    void spawnThread() {
        new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View tButton = findViewById(R.id.t_button);
        tButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                spawnThread();
            }
        });
    }
}

其實這邊發生的內存泄漏原因跟AsyncTask是一樣的。

正確的做法為:

我們自定義Thread並聲明成static這樣可以嗎?其實這樣的做法並不推薦,因為Thread位於GC根部,DVM會和所有的活動線程保持hard references關系,所以運行中的Thread絕不會被GC無端回收了,所以正確的解決辦法是在自定義靜態內部類的基礎上給線程加上取消機制,因此我們可以在Activity的onDestroy方法中將thread關閉掉。

Timer Tasks

看個範例:

public class SampleActivity extends Activity {
    void scheduleTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                while(true);
            }
        },1000);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View ttButton = findViewById(R.id.tt_button);
        ttButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            scheduleTimer();
            }
        });
    }
}

這裏內存泄漏在於Timer和TimerTask沒有進行Cancel,從而導致Timer和TimerTask一直引用外部類Activity。

正確的做法為:

在適當的時機進行Cancel。

Sensor Manager

看個範例:

public class SampleActivity extends Activity {
    void registerListener() {
           SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
           Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
           sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                registerListener();
            }
        });
    }
}

通過Context調用getSystemService獲取系統服務,這些服務運行在他們自己的進程執行一系列後臺工作或者提供和硬件交互的接口,如果Context對象需要在一個Service內部事件發生時隨時收到通知,則需要把自己作為一個監聽器註冊進去,這樣服務就會持有一個Activity,如果開發者忘記了在Activity被銷毀前註銷這個監聽器,這樣就導致內存泄漏。

正確的做法為:

在onDestroy方法裏註銷監聽器。

盡量避免使用 static 成員變量

如果成員變量被聲明為 static,那我們都知道其生命周期將與整個app進程生命周期一樣。

這會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到後臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,占內存較大的後臺進程將優先回收,如果此app做過進程互保保活,那會造成app在後臺頻繁重啟。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這裏修復的方法是:

不要在類初始時初始化靜態成員。可以考慮lazy初始化(使用時初始化)。架構設計上要思考是否真的有必要這樣做,盡量避免。如果架構需要這麽設計,那麽此對象的生命周期你有責任管理起來。

避免 override finalize()

1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是:
虛擬機調用GC的時間不確定
Finalize daemon線程被調度到的時間不確定

2、finalize 方法只會被執行一次,即使對象被復活,如果已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,原因是:

含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由於沒有了 finalize reference 與之對應,所以 finalize 方法不會再執行。

3、含有Finalize方法的object需要至少經過兩輪GC才有可能被釋放。

集合對象及時清除

我們通常會把一些對象的引用加入到集合容器(比如ArrayList)中,當我們不再需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。

所以在退出程序之前,將集合裏面的東西clear,然後置為null,再退出程序,如下:

private List<String> nameList;
private List<Fragment> list;

@Override
public void onDestroy() {
    super.onDestroy();
    if (nameList != null){
        nameList.clear();
        nameList = null;
    }
    if (list != null){
        list.clear();
        list = null;
    }
}

webView

當我們不再需要使用webView的時候,應該調用它的destory()方法來銷毀它,並釋放其占用的內存,否則其占用的內存長期也不能回收,從而造成內存泄漏。

正確的做法為:

為webView開啟另外一個進程,通過AIDL與主線程進行通信,webView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

為webView開啟另外一個進程,通過AIDL與主線程進行通信,webView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

資源未關閉

對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。

拓展 -- 相關知識點

static 關鍵字

使用static聲明屬性

如果在程序中使用static聲明屬性,則此屬性稱為全局屬性(也稱靜態屬性),那麽聲明成全局屬性有什麽用?我們看下代碼:

class Person {
    String name;
    int age;
    static String country = "A城";
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void info() {
        System.out.println("姓名:" + this.name + ",年齡:" + this.age + ",城市:" + country);
    }
};

public class Demo {
    public static void main(String agrs[]) {
        Person p1 = new Person("張三", 30);
        Person p1 = new Person("李四", 31);
        Person p1 = new Person("王五", 32);
        Person.country = "B城";
        p1.info();
        p2.info();
        p3.info();
    }
}

以上程序很清晰的說明了static聲明屬性的好處,需要註意一點的是,類的公共屬性應該由類進行修改是最合適的(當然也可以p1.country = ...),有時也就把使用static聲明的屬性稱為類屬性。

使用static聲明方法

直接看下代碼就清楚了:

class Person {
    private String name;
    private int age;
    private static String country = "A城";
    public static void setCountry(String C) {
        country = c;
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void info() {
        System.out.println("姓名:" + this.name + ",年齡:" + this.age + ",城市:" + country);
    }
    public static String getCountry() {
        return country;
    }
};

public class Demo {
    public static void main(String agrs[]) {
        Person p1 = new Person("張三", 30);
        Person p1 = new Person("李四", 31);
        Person p1 = new Person("王五", 32);
        Person.setCountry("B城");
        p1.info();
        p2.info();
        p3.info();
    }
}

【特殊說明】

??????

Android 系統開發_內存泄漏篇 -- "內存泄漏"的前世今生